/*
 * 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 (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

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

// TestChatModelAgentRun tests the Run method of ChatModelAgent
func TestChatModelAgentRun(t *testing.T) {
	// Basic test for Run method
	t.Run("BasicFunctionality", func(t *testing.T) {
		ctx := context.Background()

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Set up expectations for the mock model
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil).
			Times(1)

		// Create a ChatModelAgent
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for unit testing",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Run the agent
		input := &AgentInput{
			Messages: []Message{
				schema.UserMessage("Hello, who are you?"),
			},
		}
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get the event from the iterator
		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.Nil(t, event.Err)
		assert.NotNil(t, event.Output)
		assert.NotNil(t, event.Output.MessageOutput)

		// Verify the message content
		msg := event.Output.MessageOutput.Message
		assert.NotNil(t, msg)
		assert.Equal(t, "Hello, I am an AI assistant.", msg.Content)

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)
	})

	t.Run("BasicChatModelWithAgentMiddleware", func(t *testing.T) {
		ctx := context.Background()

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Set up expectations for the mock model
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil).
			Times(1)

		afterChatModelExecuted := false

		// Create a ChatModelAgent
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for unit testing",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			Middlewares: []AgentMiddleware{
				{
					BeforeChatModel: func(ctx context.Context, state *ChatModelAgentState) error {
						state.Messages = append(state.Messages, schema.UserMessage("m"))
						return nil
					},
					AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error {
						assert.Len(t, state.Messages, 4)
						afterChatModelExecuted = true
						return nil
					},
				},
			},
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Run the agent
		input := &AgentInput{
			Messages: []Message{
				schema.UserMessage("Hello, who are you?"),
			},
		}
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get the event from the iterator
		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.Nil(t, event.Err)
		assert.NotNil(t, event.Output)
		assert.NotNil(t, event.Output.MessageOutput)

		// Verify the message content
		msg := event.Output.MessageOutput.Message
		assert.NotNil(t, msg)
		assert.Equal(t, "Hello, I am an AI assistant.", msg.Content)

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)

		assert.True(t, afterChatModelExecuted)
	})

	t.Run("AfterChatModel_NoTools_ModifyDoesNotAffectEvent", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("original content", nil), nil).
			Times(1)

		var capturedMessages []*schema.Message

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for AfterChatModel NoTools scenario",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			Middlewares: []AgentMiddleware{
				{
					AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error {
						capturedMessages = make([]*schema.Message, len(state.Messages))
						copy(capturedMessages, state.Messages)
						state.Messages = append(state.Messages, schema.AssistantMessage("appended content", nil))
						return nil
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("Hello")},
		}
		iterator := agent.Run(ctx, input)

		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.Nil(t, event.Err)
		assert.NotNil(t, event.Output)
		assert.NotNil(t, event.Output.MessageOutput)

		msg := event.Output.MessageOutput.Message
		assert.NotNil(t, msg)
		assert.Equal(t, "original content", msg.Content)

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

		assert.Len(t, capturedMessages, 3)
	})

	t.Run("AfterChatModel_ReAct_ModifyAffectsFlow", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("first response with tool call", []schema.ToolCall{
						{ID: "tc1", Function: schema.FunctionCall{Name: "test_tool", Arguments: "{}"}},
					}), nil
				}
				return schema.AssistantMessage("final response", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		toolCalled := false
		testTool := &fakeToolForTest{tarCount: 0}

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for AfterChatModel ReAct scenario",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{testTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error {
						lastMsg := state.Messages[len(state.Messages)-1]
						if len(lastMsg.ToolCalls) > 0 {
							toolCalled = true
							state.Messages[len(state.Messages)-1] = schema.AssistantMessage("modified to remove tool call", nil)
						}
						return nil
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("Hello")},
		}
		iterator := agent.Run(ctx, input)

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

		assert.True(t, toolCalled)
		assert.Equal(t, 1, generateCount)

		assert.Equal(t, 1, len(events))
		event := events[0]
		assert.NotNil(t, event.Output)
		assert.NotNil(t, event.Output.MessageOutput)
		assert.Equal(t, "first response with tool call", event.Output.MessageOutput.Message.Content)
		assert.Len(t, event.Output.MessageOutput.Message.ToolCalls, 1)
	})

	t.Run("AfterChatModel_ReAct_AppendToolCall_AffectsFlow", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("first response no tool", nil), nil
				}
				return schema.AssistantMessage("final response", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		testTool := &fakeToolForTest{tarCount: 0}

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for AfterChatModel ReAct append tool call",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{testTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error {
						if generateCount == 1 {
							state.Messages[len(state.Messages)-1] = schema.AssistantMessage("modified with tool call", []schema.ToolCall{
								{ID: "tc1", Function: schema.FunctionCall{Name: "test_tool", Arguments: "{}"}},
							})
						}
						return nil
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("Hello")},
		}
		iterator := agent.Run(ctx, input)

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

		assert.Equal(t, 2, generateCount)

		assert.Equal(t, 3, len(events))

		event0 := events[0]
		assert.NotNil(t, event0.Output)
		assert.NotNil(t, event0.Output.MessageOutput)
		assert.Equal(t, "first response no tool", event0.Output.MessageOutput.Message.Content)
		assert.Empty(t, event0.Output.MessageOutput.Message.ToolCalls)

		event2 := events[2]
		assert.NotNil(t, event2.Output)
		assert.NotNil(t, event2.Output.MessageOutput)
		assert.Equal(t, "final response", event2.Output.MessageOutput.Message.Content)
	})

	// Test with streaming output
	t.Run("StreamOutput", func(t *testing.T) {
		ctx := context.Background()

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Create a stream reader for the mock response
		sr := schema.StreamReaderFromArray([]*schema.Message{
			schema.AssistantMessage("Hello", nil),
			schema.AssistantMessage(", I am", nil),
			schema.AssistantMessage(" an AI assistant.", nil),
		})

		// Set up expectations for the mock model
		cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(sr, nil).
			Times(1)

		// Create a ChatModelAgent
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for unit testing",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Run the agent with streaming enabled
		input := &AgentInput{
			Messages:        []Message{schema.UserMessage("Hello, who are you?")},
			EnableStreaming: true,
		}
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get the event from the iterator
		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.Nil(t, event.Err)
		assert.NotNil(t, event.Output)
		assert.NotNil(t, event.Output.MessageOutput)
		assert.True(t, event.Output.MessageOutput.IsStreaming)

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)
	})

	// Test error handling
	t.Run("ErrorHandling", func(t *testing.T) {
		ctx := context.Background()

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Set up expectations for the mock model to return an error
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(nil, errors.New("model error")).
			Times(1)

		// Create a ChatModelAgent
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for unit testing",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Run the agent
		input := &AgentInput{
			Messages: []Message{schema.UserMessage("Hello, who are you?")},
		}
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get the event from the iterator, should contain an error
		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.NotNil(t, event.Err)
		assert.Contains(t, event.Err.Error(), "model error")

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)
	})

	// Test with tools
	t.Run("WithTools", func(t *testing.T) {
		ctx := context.Background()

		// Create a fake tool for testing
		fakeTool := &fakeToolForTest{
			tarCount: 1,
		}

		info, err := fakeTool.Info(ctx)
		assert.NoError(t, err)

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Set up expectations for the mock model
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("Using tool",
				[]schema.ToolCall{
					{
						ID: "tool-call-1",
						Function: schema.FunctionCall{
							Name:      info.Name,
							Arguments: `{"name": "test user"}`,
						},
					}}), nil).
			Times(1)
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("Task completed", nil), nil).
			Times(1)
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		// Create a ChatModelAgent with tools
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for unit testing",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{fakeTool},
				},
			},
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Run the agent
		input := &AgentInput{
			Messages: []Message{schema.UserMessage("Use the test tool")},
		}
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get events from the iterator
		// First event should be the model output with tool call
		event1, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event1)
		assert.Nil(t, event1.Err)
		assert.NotNil(t, event1.Output)
		assert.NotNil(t, event1.Output.MessageOutput)
		assert.Equal(t, schema.Assistant, event1.Output.MessageOutput.Role)

		// Second event should be the tool output
		event2, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event2)
		assert.Nil(t, event2.Err)
		assert.NotNil(t, event2.Output)
		assert.NotNil(t, event2.Output.MessageOutput)
		assert.Equal(t, schema.Tool, event2.Output.MessageOutput.Role)

		// Third event should be the final model output
		event3, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event3)
		assert.Nil(t, event3.Err)
		assert.NotNil(t, event3.Output)
		assert.NotNil(t, event3.Output.MessageOutput)
		assert.Equal(t, schema.Assistant, event3.Output.MessageOutput.Role)

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)
	})

	t.Run("WrapToolCall_ToolMiddleware", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		fakeTool := &fakeToolForTest{tarCount: 1}
		info, err := fakeTool.Info(ctx)
		assert.NoError(t, err)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("calling tool", []schema.ToolCall{
						{ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}},
					}), nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		var capturedToolName string
		var capturedArgs string
		middlewareInvoked := false

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test WrapToolCall middleware",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{fakeTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					WrapToolCall: compose.ToolMiddleware{
						Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint {
							return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) {
								middlewareInvoked = true
								capturedToolName = input.Name
								capturedArgs = input.Arguments
								return next(ctx, input)
							}
						},
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("use tool")},
		}
		iterator := agent.Run(ctx, input)

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

		assert.True(t, middlewareInvoked, "WrapToolCall middleware should have been invoked")
		assert.Equal(t, info.Name, capturedToolName)
		assert.Equal(t, `{"name":"test"}`, capturedArgs)
		assert.Equal(t, 3, len(events))
		assert.Equal(t, schema.Assistant, events[0].Output.MessageOutput.Role)
		assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role)
		assert.Equal(t, schema.Assistant, events[2].Output.MessageOutput.Role)
		assert.Equal(t, "done", events[2].Output.MessageOutput.Message.Content)
	})

	t.Run("WrapToolCall_MultipleMiddlewares_Order", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		fakeTool := &fakeToolForTest{tarCount: 1}
		info, err := fakeTool.Info(ctx)
		assert.NoError(t, err)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("calling tool", []schema.ToolCall{
						{ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}},
					}), nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		var order []string

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test WrapToolCall middleware order",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{fakeTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					WrapToolCall: compose.ToolMiddleware{
						Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint {
							return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) {
								order = append(order, "mw1_before")
								out, err := next(ctx, input)
								order = append(order, "mw1_after")
								return out, err
							}
						},
					},
				},
				{
					WrapToolCall: compose.ToolMiddleware{
						Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint {
							return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) {
								order = append(order, "mw2_before")
								out, err := next(ctx, input)
								order = append(order, "mw2_after")
								return out, err
							}
						},
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("use tool")},
		}
		iterator := agent.Run(ctx, input)

		for {
			_, ok := iterator.Next()
			if !ok {
				break
			}
		}

		// First registered middleware is outermost
		assert.Equal(t, []string{"mw1_before", "mw2_before", "mw2_after", "mw1_after"}, order)
	})

	t.Run("WrapToolCall_ModifyResult", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		fakeTool := &fakeToolForTest{tarCount: 1}
		info, err := fakeTool.Info(ctx)
		assert.NoError(t, err)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("calling tool", []schema.ToolCall{
						{ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}},
					}), nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test WrapToolCall modify result",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{fakeTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					WrapToolCall: compose.ToolMiddleware{
						Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint {
							return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) {
								out, err := next(ctx, input)
								if err != nil {
									return out, err
								}
								out.Result = "modified: " + out.Result
								return out, nil
							}
						},
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("use tool")},
		}
		iterator := agent.Run(ctx, input)

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

		assert.Equal(t, 3, len(events))
		// Tool result event should have the modified content
		toolEvent := events[1]
		assert.Equal(t, schema.Tool, toolEvent.Output.MessageOutput.Role)
		assert.Contains(t, toolEvent.Output.MessageOutput.Message.Content, "modified:")
	})

	t.Run("WrapToolCall_StreamableMiddleware", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		streamTool := &fakeStreamToolForTest{tarCount: 1}
		info, err := streamTool.Info(ctx)
		assert.NoError(t, err)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("calling tool", []schema.ToolCall{
						{ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}},
					}), nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		middlewareInvoked := false

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test Streamable WrapToolCall middleware",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{streamTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					WrapToolCall: compose.ToolMiddleware{
						Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint {
							return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) {
								middlewareInvoked = true
								return next(ctx, input)
							}
						},
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("use tool")},
		}
		iterator := agent.Run(ctx, input)

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

		assert.True(t, middlewareInvoked, "Streamable WrapToolCall middleware should have been invoked")
		assert.Equal(t, 3, len(events))
		assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role)
	})

	t.Run("WrapToolCall_EnhancedInvokableMiddleware", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		enhTool := &fakeEnhancedInvokableToolForTest{}
		info, err := enhTool.Info(ctx)
		assert.NoError(t, err)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("calling tool", []schema.ToolCall{
						{ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}},
					}), nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		middlewareInvoked := false
		var capturedToolName string

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test EnhancedInvokable WrapToolCall middleware",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{enhTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					WrapToolCall: compose.ToolMiddleware{
						EnhancedInvokable: func(next compose.EnhancedInvokableToolEndpoint) compose.EnhancedInvokableToolEndpoint {
							return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
								middlewareInvoked = true
								capturedToolName = input.Name
								return next(ctx, input)
							}
						},
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("use tool")},
		}
		iterator := agent.Run(ctx, input)

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

		assert.True(t, middlewareInvoked, "EnhancedInvokable WrapToolCall middleware should have been invoked")
		assert.Equal(t, info.Name, capturedToolName)
		assert.Equal(t, 3, len(events))
		assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role)
	})

	t.Run("WrapToolCall_EnhancedStreamableMiddleware", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		enhStreamTool := &fakeEnhancedStreamableToolForTest{}
		info, err := enhStreamTool.Info(ctx)
		assert.NoError(t, err)

		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return schema.AssistantMessage("calling tool", []schema.ToolCall{
						{ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}},
					}), nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		middlewareInvoked := false

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test EnhancedStreamable WrapToolCall middleware",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{enhStreamTool},
				},
			},
			Middlewares: []AgentMiddleware{
				{
					WrapToolCall: compose.ToolMiddleware{
						EnhancedStreamable: func(next compose.EnhancedStreamableToolEndpoint) compose.EnhancedStreamableToolEndpoint {
							return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) {
								middlewareInvoked = true
								return next(ctx, input)
							}
						},
					},
				},
			},
		})
		assert.NoError(t, err)

		input := &AgentInput{
			Messages: []Message{schema.UserMessage("use tool")},
		}
		iterator := agent.Run(ctx, input)

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

		assert.True(t, middlewareInvoked, "EnhancedStreamable WrapToolCall middleware should have been invoked")
		assert.Equal(t, 3, len(events))
		assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role)
	})
}

// TestExitTool tests the Exit tool functionality
func TestExitTool(t *testing.T) {
	ctx := context.Background()

	// Create a mock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Create a mock chat model
	cm := mockModel.NewMockToolCallingChatModel(ctrl)

	// Set up expectations for the mock model
	// First call: model generates a message with Exit tool call
	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("I'll exit with a final result",
			[]schema.ToolCall{
				{
					ID: "tool-call-1",
					Function: schema.FunctionCall{
						Name:      "exit",
						Arguments: `{"final_result": "This is the final result"}`},
				},
			}), nil).
		Times(1)

	// Model should implement WithTools
	cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

	// Create an agent with the Exit tool
	agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "TestAgent",
		Description: "Test agent with Exit tool",
		Instruction: "You are a helpful assistant.",
		Model:       cm,
		Exit:        &ExitTool{},
	})
	assert.NoError(t, err)
	assert.NotNil(t, agent)

	// Run the agent
	input := &AgentInput{
		Messages: []Message{
			schema.UserMessage("Please exit with a final result"),
		},
	}
	iterator := agent.Run(ctx, input)
	assert.NotNil(t, iterator)

	// First event: model output with tool call
	event1, ok := iterator.Next()
	assert.True(t, ok)
	assert.NotNil(t, event1)
	assert.Nil(t, event1.Err)
	assert.NotNil(t, event1.Output)
	assert.NotNil(t, event1.Output.MessageOutput)
	assert.Equal(t, schema.Assistant, event1.Output.MessageOutput.Role)

	// Second event: tool output (Exit)
	event2, ok := iterator.Next()
	assert.True(t, ok)
	assert.NotNil(t, event2)
	assert.Nil(t, event2.Err)
	assert.NotNil(t, event2.Output)
	assert.NotNil(t, event2.Output.MessageOutput)
	assert.Equal(t, schema.Tool, event2.Output.MessageOutput.Role)

	// Verify the action is Exit
	assert.NotNil(t, event2.Action)
	assert.True(t, event2.Action.Exit)

	// Verify the final result
	assert.Equal(t, "This is the final result", event2.Output.MessageOutput.Message.Content)

	// No more events
	_, ok = iterator.Next()
	assert.False(t, ok)
}

func TestParallelReturnDirectlyToolCall(t *testing.T) {
	ctx := context.Background()
	// Create a mock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Create a mock chat model
	cm := mockModel.NewMockToolCallingChatModel(ctrl)

	// Set up expectations for the mock model
	// First call: model generates a message with Exit tool call
	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("I'll exit with a final result",
			[]schema.ToolCall{
				{
					ID:       "tool-call-1",
					Function: schema.FunctionCall{Name: "tool1"},
				},
				{
					ID:       "tool-call-2",
					Function: schema.FunctionCall{Name: "tool2"},
				},
				{
					ID:       "tool-call-3",
					Function: schema.FunctionCall{Name: "tool3"},
				},
			}), nil).
		Times(1)

	// Model should implement WithTools
	cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

	// Create an agent with the Exit tool
	agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "TestAgent",
		Description: "Test agent with Exit tool",
		Instruction: "You are a helpful assistant.",
		Model:       cm,
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{
					&myTool{name: "tool1", desc: "tool1", waitTime: time.Millisecond},
					&myTool{name: "tool2", desc: "tool2", waitTime: 10 * time.Millisecond},
					&myTool{name: "tool3", desc: "tool3", waitTime: 100 * time.Millisecond},
				},
			},
			ReturnDirectly: map[string]bool{
				"tool1": true,
			},
		},
	})
	assert.NoError(t, err)
	assert.NotNil(t, agent)

	r := NewRunner(ctx, RunnerConfig{
		Agent: agent,
	})
	iter := r.Query(ctx, "")
	times := 0
	for {
		e, ok := iter.Next()
		if !ok {
			assert.Equal(t, 4, times)
			break
		}
		if times == 3 {
			assert.Equal(t, "tool1", e.Output.MessageOutput.Message.ToolName)
		}
		times++
	}
}

func TestConcurrentSameToolSendToolGenActionUsesToolCallID(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	cm := mockModel.NewMockToolCallingChatModel(ctrl)

	cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("tools", []schema.ToolCall{
			{ID: "id1", Function: schema.FunctionCall{Name: "action_tool", Arguments: "A"}},
			{ID: "id2", Function: schema.FunctionCall{Name: "action_tool", Arguments: "B"}},
		}), nil).
		Times(1)

	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("done", nil), nil).
		Times(1)

	agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "TestAgent",
		Description: "Agent with action tool",
		Instruction: "",
		Model:       cm,
		ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{actionTool{}}}},
	})
	assert.NoError(t, err)

	iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}})
	seen := map[string]bool{}
	for {
		e, ok := iter.Next()
		if !ok {
			break
		}
		if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.Message != nil && e.Output.MessageOutput.Message.Role == schema.Tool {
			if e.Action != nil && e.Action.CustomizedAction != nil {
				if s, ok := e.Action.CustomizedAction.(string); ok {
					seen[s] = true
				}
			}
		}
	}
	assert.True(t, seen["A"])
	assert.True(t, seen["B"])
}

type myTool struct {
	name     string
	desc     string
	waitTime time.Duration
}

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

func (m *myTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	time.Sleep(m.waitTime)
	return "success", nil
}

type actionTool struct{}

func (a actionTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: "action_tool", Desc: "action tool"}, nil
}

func (a actionTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
	_ = SendToolGenAction(ctx, "action_tool", &AgentAction{CustomizedAction: argumentsInJSON})
	return "ok", nil
}

type streamActionTool struct{}

func (s streamActionTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: "action_tool_stream", Desc: "action stream tool"}, nil
}

func (s streamActionTool) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) {
	_ = SendToolGenAction(ctx, "action_tool_stream", &AgentAction{CustomizedAction: argumentsInJSON})
	sr, sw := schema.Pipe[string](1)
	go func() {
		defer sw.Close()
		_ = sw.Send("o", nil)
		_ = sw.Send("k", nil)
	}()
	return sr, nil
}

type legacyStreamActionTool struct{}

func (s legacyStreamActionTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: "legacy_action_tool_stream", Desc: "legacy action stream tool"}, nil
}

func (s legacyStreamActionTool) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) {
	_ = compose.ProcessState(ctx, func(ctx context.Context, st *State) error {
		st.setToolGenAction("legacy_action_tool_stream", &AgentAction{CustomizedAction: argumentsInJSON})
		return nil
	})
	sr, sw := schema.Pipe[string](1)
	go func() {
		defer sw.Close()
		_ = sw.Send("o", nil)
		_ = sw.Send("k", nil)
	}()
	return sr, nil
}

// TestChatModelAgentOutputKey tests the outputKey configuration and setOutputToSession function
func TestChatModelAgentOutputKey(t *testing.T) {
	// Test outputKey configuration - stores output in session
	t.Run("OutputKeyStoresInSession", func(t *testing.T) {
		for i := 0; i < 1000; i++ {

		}
		ctx := context.Background()

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Set up expectations for the mock model
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil).
			Times(1)

		// Create a ChatModelAgent with outputKey configured
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for unit testing",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			OutputKey:   "agent_output", // This should store output in session
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Initialize a run context to enable session storage
		input := &AgentInput{
			Messages: []Message{
				schema.UserMessage("Hello, who are you?"),
			},
		}
		ctx, runCtx := initRunCtx(ctx, "TestAgent", input)
		assert.NotNil(t, runCtx)
		assert.NotNil(t, runCtx.Session)

		// Run the agent
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get the event from the iterator
		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.Nil(t, event.Err)

		// Verify the message content
		msg := event.Output.MessageOutput.Message
		assert.Equal(t, "Hello, I am an AI assistant.", msg.Content)

		// Verify that the output was stored in the session
		time.AfterFunc(100*time.Millisecond, func() {
			sessionValues := GetSessionValues(ctx)
			assert.Contains(t, sessionValues, "agent_output")
			assert.Equal(t, "Hello, I am an AI assistant.", sessionValues["agent_output"])
		})

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)
	})

	// Test outputKey configuration with streaming - stores concatenated output in session
	t.Run("OutputKeyWithStreamingStoresInSession", func(t *testing.T) {
		ctx := context.Background()

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Create a stream reader for the mock response
		sr := schema.StreamReaderFromArray([]*schema.Message{
			schema.AssistantMessage("Hello", nil),
			schema.AssistantMessage(", I am", nil),
			schema.AssistantMessage(" an AI assistant.", nil),
		})

		// Set up expectations for the mock model
		cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(sr, nil).
			Times(1)

		// Create a ChatModelAgent with outputKey configured
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent for unit testing",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			OutputKey:   "agent_output", // This should store concatenated stream in session
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Initialize a run context to enable session storage
		input := &AgentInput{
			Messages:        []Message{schema.UserMessage("Hello, who are you?")},
			EnableStreaming: true,
		}
		ctx, runCtx := initRunCtx(ctx, "TestAgent", input)
		assert.NotNil(t, runCtx)
		assert.NotNil(t, runCtx.Session)

		// Run the agent
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get the event from the iterator
		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.Nil(t, event.Err)
		assert.True(t, event.Output.MessageOutput.IsStreaming)

		time.AfterFunc(100*time.Millisecond, func() {
			// Verify that the concatenated output was stored in the session
			sessionValues := GetSessionValues(ctx)
			assert.Contains(t, sessionValues, "agent_output")
			assert.Equal(t, "Hello, I am an AI assistant.", sessionValues["agent_output"])
		})

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)
	})

	// Test setOutputToSession function directly - regular message
	t.Run("SetOutputToSessionRegularMessage", func(t *testing.T) {
		ctx := context.Background()

		// Initialize a run context to enable session storage
		input := &AgentInput{
			Messages: []Message{schema.UserMessage("test")},
		}
		ctx, runCtx := initRunCtx(ctx, "TestAgent", input)
		assert.NotNil(t, runCtx)
		assert.NotNil(t, runCtx.Session)

		// Test with a regular message
		msg := schema.AssistantMessage("Test response", nil)
		err := setOutputToSession(ctx, msg, nil, "test_output")
		assert.NoError(t, err)

		// Verify the message content was stored
		sessionValues := GetSessionValues(ctx)
		assert.Contains(t, sessionValues, "test_output")
		assert.Equal(t, "Test response", sessionValues["test_output"])
	})

	// Test setOutputToSession function directly - streaming message
	t.Run("SetOutputToSessionStreamingMessage", func(t *testing.T) {
		ctx := context.Background()

		// Initialize a run context to enable session storage
		input := &AgentInput{
			Messages: []Message{schema.UserMessage("test")},
		}
		ctx, runCtx := initRunCtx(ctx, "TestAgent", input)
		assert.NotNil(t, runCtx)
		assert.NotNil(t, runCtx.Session)

		// Test with a streaming message
		sr := schema.StreamReaderFromArray([]*schema.Message{
			schema.AssistantMessage("Stream", nil),
			schema.AssistantMessage(" response", nil),
			schema.AssistantMessage(" content", nil),
		})
		err := setOutputToSession(ctx, nil, sr, "test_output")
		assert.NoError(t, err)

		// Verify the concatenated stream content was stored
		sessionValues := GetSessionValues(ctx)
		assert.Contains(t, sessionValues, "test_output")
		assert.Equal(t, "Stream response content", sessionValues["test_output"])
	})

	// Test setOutputToSession function directly - error case
	t.Run("SetOutputToSessionErrorCase", func(t *testing.T) {
		ctx := context.Background()

		// Initialize a run context to enable session storage
		input := &AgentInput{
			Messages: []Message{schema.UserMessage("test")},
		}
		ctx, runCtx := initRunCtx(ctx, "TestAgent", input)
		assert.NotNil(t, runCtx)
		assert.NotNil(t, runCtx.Session)

		// Test with an invalid stream (simulate error)
		// Create a stream that will fail when concatenated
		sr := schema.StreamReaderFromArray([]*schema.Message{
			schema.AssistantMessage("test", nil),
		})
		// Close the stream to simulate an error condition
		sr.Close()

		// This should return an error because the stream is closed
		err := setOutputToSession(ctx, nil, sr, "test_output")
		// Note: The actual behavior may vary depending on the stream implementation
		// Some streams may not error when closed, so we'll accept either outcome
		if err != nil {
			// If there's an error, verify nothing was stored
			sessionValues := GetSessionValues(ctx)
			assert.NotContains(t, sessionValues, "test_output")
		} else {
			// If no error, verify the content was stored
			sessionValues := GetSessionValues(ctx)
			assert.Contains(t, sessionValues, "test_output")
			assert.Equal(t, "test", sessionValues["test_output"])
		}
	})

	// Test outputKey with React workflow (tools enabled)
	t.Run("OutputKeyWithReactWorkflow", func(t *testing.T) {
		ctx := context.Background()

		// Create a mock chat model
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		// Create a simple tool for testing
		fakeTool := &fakeToolForTest{
			tarCount: 1,
		}

		// Set up expectations for the mock model - it will generate a final response
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("Final response from React workflow", nil), nil).
			Times(1)
		// Model should implement WithTools
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		// Create a ChatModelAgent with outputKey and tools configured
		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent with tools",
			Instruction: "You are a helpful assistant.",
			Model:       cm,
			OutputKey:   "agent_output",
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{fakeTool},
				},
			},
		})
		assert.NoError(t, err)
		assert.NotNil(t, agent)

		// Initialize a run context to enable session storage
		input := &AgentInput{
			Messages: []Message{schema.UserMessage("Use the tool")},
		}
		ctx, runCtx := initRunCtx(ctx, "TestAgent", input)
		assert.NotNil(t, runCtx)
		assert.NotNil(t, runCtx.Session)

		// Run the agent
		iterator := agent.Run(ctx, input)
		assert.NotNil(t, iterator)

		// Get the event from the iterator
		event, ok := iterator.Next()
		assert.True(t, ok)
		assert.NotNil(t, event)
		assert.Nil(t, event.Err)

		// Verify the message content
		msg := event.Output.MessageOutput.Message
		assert.Equal(t, "Final response from React workflow", msg.Content)

		// Verify that the output was stored in the session
		time.AfterFunc(time.Millisecond*10, func() {
			sessionValues := GetSessionValues(ctx)
			assert.Contains(t, sessionValues, "agent_output")
			assert.Equal(t, "Final response from React workflow", sessionValues["agent_output"])
		})

		// No more events
		_, ok = iterator.Next()
		assert.False(t, ok)
	})
}

func TestConcurrentSameStreamToolSendToolGenActionUsesToolCallID(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	cm := mockModel.NewMockToolCallingChatModel(ctrl)

	cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("tools", []schema.ToolCall{
			{ID: "sid1", Function: schema.FunctionCall{Name: "action_tool_stream", Arguments: "SA"}},
			{ID: "sid2", Function: schema.FunctionCall{Name: "action_tool_stream", Arguments: "SB"}},
		}), nil).
		Times(1)

	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("done", nil), nil).
		Times(1)

	agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "TestAgent",
		Description: "Agent with stream action tool",
		Instruction: "",
		Model:       cm,
		ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{streamActionTool{}}}},
	})
	assert.NoError(t, err)

	iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}})
	seen := map[string]bool{}
	for {
		e, ok := iter.Next()
		if !ok {
			break
		}
		if e.Output != nil && e.Output.MessageOutput != nil {
			if e.Output.MessageOutput.IsStreaming {
				if e.Action != nil && e.Action.CustomizedAction != nil {
					if s, ok := e.Action.CustomizedAction.(string); ok {
						seen[s] = true
					}
				}
			}
		}
	}
	assert.True(t, seen["SA"])
	assert.True(t, seen["SB"])
}

func TestStreamToolLegacyNameKeyFallback(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	cm := mockModel.NewMockToolCallingChatModel(ctrl)
	cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("tools", []schema.ToolCall{
			{ID: "lsid1", Function: schema.FunctionCall{Name: "legacy_action_tool_stream", Arguments: "LA"}},
		}), nil).
		Times(1)

	cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(schema.AssistantMessage("done", nil), nil).
		Times(1)

	agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "TestAgent",
		Description: "Agent with legacy stream action tool",
		Instruction: "",
		Model:       cm,
		ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{legacyStreamActionTool{}}}},
	})
	assert.NoError(t, err)

	iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}})
	found := false
	for {
		e, ok := iter.Next()
		if !ok {
			break
		}
		if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.IsStreaming {
			if e.Action != nil && e.Action.CustomizedAction != nil {
				if s, ok := e.Action.CustomizedAction.(string); ok {
					found = (s == "LA")
				}
			}
		}
	}
	assert.True(t, found)
}

func TestChatModelAgent_ToolResultMiddleware_EmitsFinalResult(t *testing.T) {
	originalResult := "original_result"
	modifiedResult := "modified_by_middleware"

	resultModifyingMiddleware := compose.ToolMiddleware{
		Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint {
			return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) {
				output, err := next(ctx, input)
				if err != nil {
					return nil, err
				}
				output.Result = modifiedResult
				return output, nil
			}
		},
		Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint {
			return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) {
				output, err := next(ctx, input)
				if err != nil {
					return nil, err
				}
				output.Result = schema.StreamReaderFromArray([]string{modifiedResult})
				return output, nil
			}
		},
	}

	t.Run("Invoke", func(t *testing.T) {
		ctx := context.Background()
		testTool := &simpleToolForMiddlewareTest{name: "test_tool", result: originalResult}

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		info, err := testTool.Info(ctx)
		assert.NoError(t, err)

		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("",
				[]schema.ToolCall{
					{
						ID: "tool-call-1",
						Function: schema.FunctionCall{
							Name:      info.Name,
							Arguments: `{"input": "test"}`,
						},
					},
				}), nil).
			Times(1)
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("final response", nil), nil).
			Times(1)
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "test_agent",
			Description: "test agent with middleware",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools:               []tool.BaseTool{testTool},
					ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware},
				},
			},
		})
		assert.NoError(t, err)

		r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false, CheckPointStore: newBridgeStore()})
		it := r.Run(ctx, []Message{schema.UserMessage("call the tool")})

		var toolResultEvents []*AgentEvent
		for {
			ev, ok := it.Next()
			if !ok {
				break
			}
			if ev.Output != nil && ev.Output.MessageOutput != nil &&
				ev.Output.MessageOutput.Message != nil &&
				ev.Output.MessageOutput.Message.Role == schema.Tool {
				toolResultEvents = append(toolResultEvents, ev)
			}
		}

		assert.NotEmpty(t, toolResultEvents, "should have at least one tool result event")
		for _, ev := range toolResultEvents {
			assert.Equal(t, modifiedResult, ev.Output.MessageOutput.Message.Content,
				"tool result event should contain the middleware-modified result, not the original")
			assert.NotEqual(t, originalResult, ev.Output.MessageOutput.Message.Content,
				"tool result event should NOT contain the original result")
		}
	})

	t.Run("Stream", func(t *testing.T) {
		ctx := context.Background()
		testTool := &simpleToolForMiddlewareTest{name: "test_tool_stream", result: originalResult}

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		info, err := testTool.Info(ctx)
		assert.NoError(t, err)

		cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.StreamReaderFromArray([]*schema.Message{
				schema.AssistantMessage("", []schema.ToolCall{
					{
						ID: "tool-call-1",
						Function: schema.FunctionCall{
							Name:      info.Name,
							Arguments: `{"input": "test"}`,
						},
					},
				}),
			}), nil).
			Times(1)
		cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.StreamReaderFromArray([]*schema.Message{
				schema.AssistantMessage("final response", nil),
			}), nil).
			Times(1)
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "test_agent",
			Description: "test agent with middleware",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools:               []tool.BaseTool{testTool},
					ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware},
				},
			},
		})
		assert.NoError(t, err)

		r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true, CheckPointStore: newBridgeStore()})
		it := r.Run(ctx, []Message{schema.UserMessage("call the tool")})

		var toolResultContents []string
		for {
			ev, ok := it.Next()
			if !ok {
				break
			}
			if ev.Output != nil && ev.Output.MessageOutput != nil {
				if ev.Output.MessageOutput.Message != nil &&
					ev.Output.MessageOutput.Message.Role == schema.Tool {
					toolResultContents = append(toolResultContents, ev.Output.MessageOutput.Message.Content)
				}
				if ev.Output.MessageOutput.IsStreaming &&
					ev.Output.MessageOutput.MessageStream != nil &&
					ev.Output.MessageOutput.Role == schema.Tool {
					var msgs []*schema.Message
					for {
						msg, err := ev.Output.MessageOutput.MessageStream.Recv()
						if err != nil {
							break
						}
						msgs = append(msgs, msg)
					}
					if len(msgs) > 0 {
						concated, err := schema.ConcatMessages(msgs)
						if err == nil {
							toolResultContents = append(toolResultContents, concated.Content)
						}
					}
				}
			}
		}

		assert.NotEmpty(t, toolResultContents, "should have at least one tool result event")
		for _, content := range toolResultContents {
			assert.Equal(t, modifiedResult, content,
				"tool result event should contain the middleware-modified result, not the original")
			assert.NotEqual(t, originalResult, content,
				"tool result event should NOT contain the original result")
		}
	})
}

type simpleToolForMiddlewareTest struct {
	name   string
	result string
}

func (s *simpleToolForMiddlewareTest) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: s.name,
		Desc: "simple tool",
		ParamsOneOf: schema.NewParamsOneOfByParams(
			map[string]*schema.ParameterInfo{
				"input": {
					Desc:     "input",
					Required: true,
					Type:     schema.String,
				},
			}),
	}, nil
}

func (s *simpleToolForMiddlewareTest) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) {
	return s.result, nil
}

func (s *simpleToolForMiddlewareTest) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) {
	return schema.StreamReaderFromArray([]string{s.result}), nil
}

func TestGetComposeOptions(t *testing.T) {
	t.Run("WithChatModelOptions", func(t *testing.T) {
		ctx := context.Background()
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		var capturedTemperature float32
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				options := model.GetCommonOptions(&model.Options{}, opts...)
				if options.Temperature != nil {
					capturedTemperature = *options.Temperature
				}
				return schema.AssistantMessage("response", nil), nil
			}).Times(1)

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent",
			Model:       cm,
		})
		assert.NoError(t, err)

		temp := float32(0.7)
		iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}},
			WithChatModelOptions([]model.Option{model.WithTemperature(temp)}))
		for {
			_, ok := iter.Next()
			if !ok {
				break
			}
		}

		assert.Equal(t, temp, capturedTemperature, "Temperature should be passed through WithChatModelOptions")
	})

	t.Run("WithToolOptions", func(t *testing.T) {
		ctx := context.Background()
		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		var toolOptionsCaptured bool
		testTool := &toolOptionCapturingTool{
			name: "test_tool",
			onRun: func(opts []tool.Option) {
				if len(opts) > 0 {
					toolOptionsCaptured = true
				}
			},
		}
		info, _ := testTool.Info(ctx)

		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("Using tool", []schema.ToolCall{
				{ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}},
			}), nil).Times(1)
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(schema.AssistantMessage("done", nil), nil).Times(1)

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{testTool},
				},
			},
		})
		assert.NoError(t, err)

		iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}},
			WithToolOptions([]tool.Option{testToolOption("test_value")}))
		for {
			_, ok := iter.Next()
			if !ok {
				break
			}
		}

		assert.True(t, toolOptionsCaptured, "Tool options should be passed through WithToolOptions")
	})

}

type toolOptionCapturingTool struct {
	name  string
	onRun func(opts []tool.Option)
}

func (t *toolOptionCapturingTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: t.name, Desc: t.name + " description"}, nil
}

func (t *toolOptionCapturingTool) InvokableRun(_ context.Context, _ string, opts ...tool.Option) (string, error) {
	if t.onRun != nil {
		t.onRun(opts)
	}
	return t.name + " result", nil
}

type testToolOptions struct {
	value string
}

func testToolOption(value string) tool.Option {
	return tool.WrapImplSpecificOptFn(func(o *testToolOptions) {
		o.value = value
	})
}

type errorTool struct {
	infoErr error
}

func (e *errorTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return nil, e.infoErr
}

func (e *errorTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) {
	return "", nil
}

func TestChatModelAgent_PrepareExecContextError(t *testing.T) {
	t.Run("Run_WithToolInfoError_ReturnsError", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		expectedErr := errors.New("tool info error")
		errTool := &errorTool{infoErr: expectedErr}

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{errTool},
				},
			},
		})
		assert.NoError(t, err)

		iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}})

		event, ok := iter.Next()
		assert.True(t, ok)
		assert.NotNil(t, event.Err)
		assert.Contains(t, event.Err.Error(), "tool info error")

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

	t.Run("Resume_WithToolInfoError_ReturnsError", func(t *testing.T) {
		ctx := context.Background()

		ctrl := gomock.NewController(t)
		cm := mockModel.NewMockToolCallingChatModel(ctrl)

		expectedErr := errors.New("tool info error for resume")
		errTool := &errorTool{infoErr: expectedErr}

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "Test agent",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{errTool},
				},
			},
		})
		assert.NoError(t, err)

		iter := agent.Resume(ctx, &ResumeInfo{
			InterruptState:  []byte("dummy"),
			EnableStreaming: false,
		})

		event, ok := iter.Next()
		assert.True(t, ok)
		assert.NotNil(t, event.Err)
		assert.Contains(t, event.Err.Error(), "tool info error for resume")

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

// fakeEnhancedInvokableToolForTest implements tool.EnhancedInvokableTool for testing.
type fakeEnhancedInvokableToolForTest struct{}

func (t *fakeEnhancedInvokableToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: "test_enhanced_invokable_tool",
		Desc: "test enhanced invokable tool",
	}, nil
}

func (t *fakeEnhancedInvokableToolForTest) InvokableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) {
	return &schema.ToolResult{
		Parts: []schema.ToolOutputPart{
			{Type: schema.ToolPartTypeText, Text: "enhanced invokable result"},
		},
	}, nil
}

// fakeEnhancedStreamableToolForTest implements tool.EnhancedStreamableTool for testing.
type fakeEnhancedStreamableToolForTest struct{}

func (t *fakeEnhancedStreamableToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: "test_enhanced_streamable_tool",
		Desc: "test enhanced streamable tool",
	}, nil
}

func (t *fakeEnhancedStreamableToolForTest) StreamableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
	sr := schema.StreamReaderFromArray([]*schema.ToolResult{
		{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "enhanced streamable result"}}},
	})
	return sr, nil
}

func TestPreprocessComposeCheckpoint_MigrateErrorIsReturned(t *testing.T) {
	in := []byte("prefix\u0015" + stateGobNameV080 + "suffix")
	_, err := preprocessComposeCheckpoint(in)
	assert.Error(t, err)
}

func TestNewChatModelAgent_FailoverConfigValidation(t *testing.T) {
	ctx := context.Background()
	cm := &fakeChatModel{
		callbacksEnabled: true,
		generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
			return schema.AssistantMessage("ok", nil), nil
		},
		stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
			return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok", nil)}), nil
		},
	}

	t.Run("missing GetFailoverModel", func(t *testing.T) {
		_, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "test",
			Model:       cm,
			ModelFailoverConfig: &ModelFailoverConfig[*schema.Message]{
				ShouldFailover: func(context.Context, *schema.Message, error) bool { return true },
			},
		})
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "ModelFailoverConfig.GetFailoverModel")
	})

	t.Run("missing ShouldFailover", func(t *testing.T) {
		_, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "TestAgent",
			Description: "test",
			Model:       cm,
			ModelFailoverConfig: &ModelFailoverConfig[*schema.Message]{
				GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) {
					return cm, nil, nil
				},
			},
		})
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "ModelFailoverConfig.ShouldFailover")
	})
}

// aliasCaptureTool captures the raw arguments JSON received by the tool.
type aliasCaptureTool struct {
	name         string
	params       map[string]*schema.ParameterInfo
	receivedArgs string
}

func (t *aliasCaptureTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name:        t.name,
		Desc:        t.name + " tool",
		ParamsOneOf: schema.NewParamsOneOfByParams(t.params),
	}, nil
}

func (t *aliasCaptureTool) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
	t.receivedArgs = argumentsInJSON
	return "ok", nil
}

func TestToolAliasesPropagation(t *testing.T) {
	t.Run("prepareExecContext_propagates_ToolAliases", func(t *testing.T) {
		ctx := context.Background()
		ctrl := gomock.NewController(t)

		captureTool := &aliasCaptureTool{
			name: "grep",
			params: map[string]*schema.ParameterInfo{
				"pattern": {Type: schema.String, Desc: "regex pattern"},
				"path":    {Type: schema.String, Desc: "search path"},
			},
		}

		cm := mockModel.NewMockToolCallingChatModel(ctrl)
		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return &schema.Message{
						Role: schema.Assistant,
						ToolCalls: []schema.ToolCall{
							{
								ID: "call_1",
								Function: schema.FunctionCall{
									Name:      "grep",
									Arguments: `{"grep_content": "TODO", "path": "/src"}`,
								},
							},
						},
					}, nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "test",
			Instruction: "test",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{captureTool},
					ToolAliases: map[string]compose.ToolAliasConfig{
						"grep": {
							ArgumentsAliases: map[string][]string{
								"pattern": {"grep_content"},
							},
						},
					},
				},
			},
		})
		require.NoError(t, err)

		iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search for TODOs")}})
		for {
			_, ok := iter.Next()
			if !ok {
				break
			}
		}

		require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called")
		var args map[string]any
		err = json.Unmarshal([]byte(captureTool.receivedArgs), &args)
		require.NoError(t, err)
		assert.Equal(t, "TODO", args["pattern"], "alias 'grep_content' should be remapped to 'pattern'")
		assert.NotContains(t, args, "grep_content", "alias key should not be present after remapping")
		assert.Equal(t, "/src", args["path"])
	})

	t.Run("applyBeforeAgent_preserves_ToolAliases_when_handler_modifies_tools", func(t *testing.T) {
		ctx := context.Background()
		ctrl := gomock.NewController(t)

		captureTool := &aliasCaptureTool{
			name: "grep",
			params: map[string]*schema.ParameterInfo{
				"pattern": {Type: schema.String, Desc: "regex pattern"},
			},
		}

		extraTool := &aliasCaptureTool{
			name: "extra_tool",
			params: map[string]*schema.ParameterInfo{
				"input": {Type: schema.String},
			},
		}

		cm := mockModel.NewMockToolCallingChatModel(ctrl)
		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return &schema.Message{
						Role: schema.Assistant,
						ToolCalls: []schema.ToolCall{
							{
								ID: "call_1",
								Function: schema.FunctionCall{
									Name:      "grep",
									Arguments: `{"grep_content": "FIXME"}`,
								},
							},
						},
					}, nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		handler := &testToolsHandler{
			BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
			tools:                        []tool.BaseTool{extraTool},
		}

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "test",
			Instruction: "test",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{captureTool},
					ToolAliases: map[string]compose.ToolAliasConfig{
						"grep": {
							ArgumentsAliases: map[string][]string{
								"pattern": {"grep_content"},
							},
						},
					},
				},
			},
			Handlers: []ChatModelAgentMiddleware{handler},
		})
		require.NoError(t, err)

		iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search for FIXMEs")}})
		for {
			_, ok := iter.Next()
			if !ok {
				break
			}
		}

		require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called")
		var args map[string]any
		err = json.Unmarshal([]byte(captureTool.receivedArgs), &args)
		require.NoError(t, err)
		assert.Equal(t, "FIXME", args["pattern"], "alias 'grep_content' should be remapped to 'pattern' even after handler rebuild")
		assert.NotContains(t, args, "grep_content", "alias key should not be present after remapping")
	})

	t.Run("name_alias_propagated_through_prepareExecContext", func(t *testing.T) {
		ctx := context.Background()
		ctrl := gomock.NewController(t)

		captureTool := &aliasCaptureTool{
			name: "grep",
			params: map[string]*schema.ParameterInfo{
				"pattern": {Type: schema.String},
			},
		}

		cm := mockModel.NewMockToolCallingChatModel(ctrl)
		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return &schema.Message{
						Role: schema.Assistant,
						ToolCalls: []schema.ToolCall{
							{
								ID: "call_1",
								Function: schema.FunctionCall{
									Name:      "search_content",
									Arguments: `{"pattern": "TODO"}`,
								},
							},
						},
					}, nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "test",
			Instruction: "test",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					Tools: []tool.BaseTool{captureTool},
					ToolAliases: map[string]compose.ToolAliasConfig{
						"grep": {
							NameAliases: []string{"search_content"},
						},
					},
				},
			},
		})
		require.NoError(t, err)

		iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search")}})
		for {
			_, ok := iter.Next()
			if !ok {
				break
			}
		}

		require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called via name alias 'search_content'")
		var args map[string]any
		err = json.Unmarshal([]byte(captureTool.receivedArgs), &args)
		require.NoError(t, err)
		assert.Equal(t, "TODO", args["pattern"])
	})

	t.Run("handler_adds_tool_matching_preexisting_ToolAliases_with_no_initial_tools", func(t *testing.T) {
		ctx := context.Background()
		ctrl := gomock.NewController(t)

		captureTool := &aliasCaptureTool{
			name: "grep",
			params: map[string]*schema.ParameterInfo{
				"pattern": {Type: schema.String, Desc: "regex pattern"},
			},
		}

		cm := mockModel.NewMockToolCallingChatModel(ctrl)
		generateCount := 0
		cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
				generateCount++
				if generateCount == 1 {
					return &schema.Message{
						Role: schema.Assistant,
						ToolCalls: []schema.ToolCall{
							{
								ID: "call_1",
								Function: schema.FunctionCall{
									Name:      "grep",
									Arguments: `{"grep_content": "BUG"}`,
								},
							},
						},
					}, nil
				}
				return schema.AssistantMessage("done", nil), nil
			}).AnyTimes()
		cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()

		handler := &testToolsHandler{
			BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
			tools:                        []tool.BaseTool{captureTool},
		}

		agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
			Name:        "test",
			Instruction: "test",
			Model:       cm,
			ToolsConfig: ToolsConfig{
				ToolsNodeConfig: compose.ToolsNodeConfig{
					ToolAliases: map[string]compose.ToolAliasConfig{
						"grep": {
							ArgumentsAliases: map[string][]string{
								"pattern": {"grep_content"},
							},
						},
					},
				},
			},
			Handlers: []ChatModelAgentMiddleware{handler},
		})
		require.NoError(t, err)

		iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("find bugs")}})
		for {
			_, ok := iter.Next()
			if !ok {
				break
			}
		}

		require.NotEmpty(t, captureTool.receivedArgs, "tool added by handler should have been called")
		var args map[string]any
		err = json.Unmarshal([]byte(captureTool.receivedArgs), &args)
		require.NoError(t, err)
		assert.Equal(t, "BUG", args["pattern"], "alias 'grep_content' should be remapped to 'pattern' for handler-added tool")
		assert.NotContains(t, args, "grep_content")
	})
}
