/*
 * Copyright 2024 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 compose

import (
	"context"
	"fmt"
	"io"
	"strconv"
	"strings"
	"testing"

	"github.com/bytedance/sonic"
	"github.com/stretchr/testify/assert"

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

const (
	toolNameOfUserCompany = "user_company"
	toolIDOfUserCompany   = "call_TRZhlagwBS0LpWbWPeZOvIXc"

	toolNameOfUserSalary = "user_salary"
	toolIDOfUserSalary   = "call_AqfoRW6fuF98k0o7696k2nzm"
)

func TestToolsNode(t *testing.T) {
	var err error
	ctx := context.Background()

	userCompanyToolInfo := &schema.ToolInfo{
		Name: toolNameOfUserCompany,
		Desc: "Query user's company and position information based on user's name and email",
		ParamsOneOf: schema.NewParamsOneOfByParams(
			map[string]*schema.ParameterInfo{
				"name": {
					Type: "string",
					Desc: "User's name",
				},
				"email": {
					Type: "string",
					Desc: "User's email",
				},
			}),
	}

	userSalaryToolInfo := &schema.ToolInfo{
		Name: toolNameOfUserSalary,
		Desc: "Query user's salary information based on user's name and email",
		ParamsOneOf: schema.NewParamsOneOfByParams(
			map[string]*schema.ParameterInfo{
				"name": {
					Type: "string",
					Desc: "User's name",
				},
				"email": {
					Type: "string",
					Desc: "User's email",
				},
			}),
	}

	t.Run("success", func(t *testing.T) {
		const (
			nodeOfTools = "tools"
			nodeOfModel = "model"
		)
		g := NewGraph[[]*schema.Message, []*schema.Message]()

		err = g.AddChatModelNode(nodeOfModel, &mockIntentChatModel{})
		assert.NoError(t, err)

		ui := newTool(userCompanyToolInfo, queryUserCompany)
		us := newStreamableTool(userSalaryToolInfo, queryUserSalary)

		toolsNode, err := NewToolNode(ctx, &ToolsNodeConfig{
			Tools: []tool.BaseTool{ui, us},
		})
		assert.NoError(t, err)

		err = g.AddToolsNode(nodeOfTools, toolsNode)
		assert.NoError(t, err)

		err = g.AddEdge(START, nodeOfModel)
		assert.NoError(t, err)

		err = g.AddEdge(nodeOfModel, nodeOfTools)
		assert.NoError(t, err)

		err = g.AddEdge(nodeOfTools, END)
		assert.NoError(t, err)

		r, err := g.Compile(ctx)
		assert.NoError(t, err)

		out, err := r.Invoke(ctx, []*schema.Message{})
		assert.NoError(t, err)

		msg := findMsgByToolCallID(out, toolIDOfUserCompany)
		assert.Equal(t, toolIDOfUserCompany, msg.ToolCallID)
		assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","gender":"male","company":"bytedance","position":"CEO"}`,
			msg.Content)

		msg = findMsgByToolCallID(out, toolIDOfUserSalary)
		assert.Equal(t, toolIDOfUserSalary, msg.ToolCallID)
		assert.Contains(t, msg.Content,
			`{"user_id":"zhangsan-zhangsan@bytedance.com","salary":5000}{"user_id":"zhangsan-zhangsan@bytedance.com","salary":3000}{"user_id":"zhangsan-zhangsan@bytedance.com","salary":2000}`)

		// 测试流式调用
		reader, err := r.Stream(ctx, []*schema.Message{})
		assert.NoError(t, err)
		loops := 0
		userSalaryTimes := 0

		defer reader.Close()

		var arrMsgs [][]*schema.Message
		for ; loops < 10; loops++ {
			msgs, err := reader.Recv()
			if err == io.EOF {
				break
			}

			arrMsgs = append(arrMsgs, msgs)

			assert.NoError(t, err)

			assert.Len(t, msgs, 2)
			if msg := findMsgByToolCallID(out, toolIDOfUserCompany); msg != nil {
				assert.Equal(t, schema.Tool, msg.Role)
				assert.Equal(t, toolIDOfUserCompany, msg.ToolCallID)
				assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","gender":"male","company":"bytedance","position":"CEO"}`,
					msg.Content)
			} else if msg := findMsgByToolCallID(out, toolIDOfUserSalary); msg != nil {
				assert.Equal(t, schema.Tool, msg.Role)
				assert.Equal(t, toolIDOfUserSalary, msg.ToolCallID)

				switch userSalaryTimes {
				case 0:
					assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":5000}`,
						msg.Content)
				case 1:
					assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":3000}`,
						msg.Content)
				case 2:
					assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":2000}`,
						msg.Content)
				}

				userSalaryTimes++
			} else {
				assert.Fail(t, "unexpected tool name")
			}
		}

		assert.Equal(t, 4, loops)

		msgs, err_ := schema.ConcatMessageArray(arrMsgs)
		assert.NoError(t, err_)
		msg = findMsgByToolCallID(msgs, toolIDOfUserCompany)
		msg = findMsgByToolCallID(msgs, toolIDOfUserSalary)

		sr, sw := schema.Pipe[[]*schema.Message](2)
		sw.Send([]*schema.Message{
			{
				Role:    schema.User,
				Content: `hi, how are you`,
			},
		}, nil)
		sw.Send([]*schema.Message{
			{
				Role:    schema.User,
				Content: `i'm fine'`,
			},
		}, nil)
		sw.Close()

		reader, err = r.Transform(ctx, sr)
		assert.NoError(t, err)

		defer reader.Close()

		loops = 0
		userSalaryTimes = 0

		for ; loops < 10; loops++ {
			msgs, err := reader.Recv()
			if err == io.EOF {
				break
			}

			assert.NoError(t, err)

			assert.Len(t, msgs, 2)
			if msg := findMsgByToolCallID(out, toolIDOfUserCompany); msg != nil {
				assert.Equal(t, schema.Tool, msg.Role)
				assert.Equal(t, toolIDOfUserCompany, msg.ToolCallID)
				assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","gender":"male","company":"bytedance","position":"CEO"}`,
					msg.Content)
			} else if msg := findMsgByToolCallID(out, toolIDOfUserSalary); msg != nil {
				assert.Equal(t, schema.Tool, msg.Role)
				assert.Equal(t, toolIDOfUserSalary, msg.ToolCallID)

				switch userSalaryTimes {
				case 0:
					assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":5000}`,
						msg.Content)
				case 1:
					assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":3000}`,
						msg.Content)
				case 2:
					assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":2000}`,
						msg.Content)
				}

				userSalaryTimes++
			} else {
				assert.Fail(t, "unexpected tool name")
			}
		}

		assert.Equal(t, 4, loops)
	})

	t.Run("order_consistency", func(t *testing.T) {
		// Create a ToolsNode with multiple tools
		ui := newTool(userCompanyToolInfo, queryUserCompany)
		us := newTool(userSalaryToolInfo, queryUserSalary)

		toolsNode, err_ := NewToolNode(context.Background(), &ToolsNodeConfig{
			Tools: []tool.BaseTool{ui, us},
		})
		assert.NoError(t, err_)

		// Create an input message with multiple tool calls in a specific order
		input := &schema.Message{
			Role: schema.Assistant,
			ToolCalls: []schema.ToolCall{
				{
					ID: toolIDOfUserSalary,
					Function: schema.FunctionCall{
						Name:      toolNameOfUserSalary,
						Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`,
					},
				},
				{
					ID: toolIDOfUserCompany,
					Function: schema.FunctionCall{
						Name:      toolNameOfUserCompany,
						Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`,
					},
				},
			},
		}

		// Invoke the ToolsNode
		output, err_ := toolsNode.Invoke(context.Background(), input)
		assert.NoError(t, err_)

		// Verify the order of output messages matches the order of input tool calls
		assert.Equal(t, 2, len(output))
		assert.Equal(t, toolIDOfUserSalary, output[0].ToolCallID)
		assert.Equal(t, toolIDOfUserCompany, output[1].ToolCallID)

		// Test with Stream method as well
		streamer, err_ := toolsNode.Stream(context.Background(), input)
		assert.NoError(t, err_)
		defer streamer.Close()

		// Collect all stream outputs
		var streamOutputs [][]*schema.Message
		for {
			chunk, err__ := streamer.Recv()
			if err__ == io.EOF {
				break
			}
			assert.NoError(t, err__)
			streamOutputs = append(streamOutputs, chunk)
		}

		// Verify each chunk maintains the correct order
		for _, chunk := range streamOutputs {
			if chunk[0] != nil {
				assert.Equal(t, toolIDOfUserSalary, chunk[0].ToolCallID)
			}
			if chunk[1] != nil {
				assert.Equal(t, toolIDOfUserCompany, chunk[1].ToolCallID)
			}
		}

		// Concatenate all stream outputs and verify final result
		concatenated, err_ := schema.ConcatMessageArray(streamOutputs)
		assert.NoError(t, err_)
		assert.Equal(t, 2, len(concatenated))
		assert.Equal(t, toolIDOfUserSalary, concatenated[0].ToolCallID)
		assert.Equal(t, toolIDOfUserCompany, concatenated[1].ToolCallID)
	})
}

type userCompanyRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

type userCompanyResponse struct {
	UserID   string `json:"user_id"`
	Gender   string `json:"gender"`
	Company  string `json:"company"`
	Position string `json:"position"`
}

func queryUserCompany(ctx context.Context, req *userCompanyRequest) (resp *userCompanyResponse, err error) {
	callID := GetToolCallID(ctx)
	if callID != toolIDOfUserCompany {
		return nil, fmt.Errorf("invalid tool call id= %s", callID)
	}

	return &userCompanyResponse{
		UserID:   fmt.Sprintf("%v-%v", req.Name, req.Email),
		Gender:   "male",
		Company:  "bytedance",
		Position: "CEO",
	}, nil
}

type userSalaryRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

type userSalaryResponse struct {
	UserID string `json:"user_id"`
	Salary int    `json:"salary"`
}

func queryUserSalary(ctx context.Context, req *userSalaryRequest) (resp *schema.StreamReader[*userSalaryResponse], err error) {
	callID := GetToolCallID(ctx)
	if callID != toolIDOfUserSalary {
		return nil, fmt.Errorf("invalid tool call id= %s", callID)
	}

	sr, sw := schema.Pipe[*userSalaryResponse](10)
	sw.Send(&userSalaryResponse{
		UserID: fmt.Sprintf("%v-%v", req.Name, req.Email),
		Salary: 5000,
	}, nil)

	sw.Send(&userSalaryResponse{
		UserID: fmt.Sprintf("%v-%v", req.Name, req.Email),
		Salary: 3000,
	}, nil)

	sw.Send(&userSalaryResponse{
		UserID: fmt.Sprintf("%v-%v", req.Name, req.Email),
		Salary: 2000,
	}, nil)
	sw.Close()
	return sr, nil
}

type mockIntentChatModel struct{}

func (m *mockIntentChatModel) BindTools(tools []*schema.ToolInfo) error {
	return nil
}

func (m *mockIntentChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
	return &schema.Message{
		Role:    schema.Assistant,
		Content: "",
		ToolCalls: []schema.ToolCall{
			{
				ID: toolIDOfUserCompany,
				Function: schema.FunctionCall{
					Name:      toolNameOfUserCompany,
					Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`,
				},
			},
			{
				ID: toolIDOfUserSalary,
				Function: schema.FunctionCall{
					Name:      toolNameOfUserSalary,
					Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`,
				},
			},
		},
	}, nil
}

func (m *mockIntentChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	sr, sw := schema.Pipe[*schema.Message](2)
	sw.Send(&schema.Message{
		Role:    schema.Assistant,
		Content: "",
		ToolCalls: []schema.ToolCall{
			{
				ID: toolIDOfUserCompany,
				Function: schema.FunctionCall{
					Name:      toolNameOfUserCompany,
					Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`,
				},
			},
		},
	}, nil)

	sw.Send(&schema.Message{
		Role:    schema.Assistant,
		Content: "",
		ToolCalls: []schema.ToolCall{
			{
				ID: toolIDOfUserSalary,
				Function: schema.FunctionCall{
					Name:      toolNameOfUserSalary,
					Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`,
				},
			},
		},
	}, nil)

	sw.Close()

	return sr, nil
}

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

	t.Run("tool_option", func(t *testing.T) {

		g := NewGraph[*schema.Message, []*schema.Message]()

		mt := &mockTool{}

		tn, err := NewToolNode(ctx, &ToolsNodeConfig{
			Tools: []tool.BaseTool{mt},
		})
		assert.NoError(t, err)

		err = g.AddToolsNode("tools", tn)
		assert.NoError(t, err)

		err = g.AddEdge(START, "tools")
		assert.NoError(t, err)
		err = g.AddEdge("tools", END)
		assert.NoError(t, err)

		r, err := g.Compile(ctx)
		assert.NoError(t, err)

		out, err := r.Invoke(ctx, &schema.Message{
			Role: schema.Assistant,
			ToolCalls: []schema.ToolCall{
				{
					ID: toolIDOfUserCompany,
					Function: schema.FunctionCall{
						Name:      "mock_tool",
						Arguments: `{"name": "jack"}`,
					},
				},
			},
		}, WithToolsNodeOption(WithToolOption(WithAge(10))))
		assert.NoError(t, err)
		assert.Len(t, out, 1)
		assert.JSONEq(t, `{"echo": "jack: 10"}`, out[0].Content)

		outMessages := make([][]*schema.Message, 0)
		outStream, err := r.Stream(ctx, &schema.Message{
			Role: schema.Assistant,
			ToolCalls: []schema.ToolCall{
				{
					ID: toolIDOfUserCompany,
					Function: schema.FunctionCall{
						Name:      "mock_tool",
						Arguments: `{"name": "jack"}`,
					},
				},
			},
		}, WithToolsNodeOption(WithToolOption(WithAge(10))))

		assert.NoError(t, err)

		for {
			msgs, err := outStream.Recv()
			if err == io.EOF {
				break
			}
			assert.NoError(t, err)
			outMessages = append(outMessages, msgs)
		}
		outStream.Close()

		msgs, err := internal.ConcatItems(outMessages)
		assert.NoError(t, err)

		assert.Len(t, msgs, 1)
		assert.JSONEq(t, `{"echo":"jack: 10"}`, msgs[0].Content)
	})
	t.Run("tool_list", func(t *testing.T) {

		g := NewGraph[*schema.Message, []*schema.Message]()

		mt := &mockTool{}

		tn, err := NewToolNode(ctx, &ToolsNodeConfig{
			Tools: []tool.BaseTool{},
		})
		assert.NoError(t, err)

		err = g.AddToolsNode("tools", tn)
		assert.NoError(t, err)

		err = g.AddEdge(START, "tools")
		assert.NoError(t, err)
		err = g.AddEdge("tools", END)
		assert.NoError(t, err)

		r, err := g.Compile(ctx)
		assert.NoError(t, err)

		out, err := r.Invoke(ctx, &schema.Message{
			Role: schema.Assistant,
			ToolCalls: []schema.ToolCall{
				{
					ID: toolIDOfUserCompany,
					Function: schema.FunctionCall{
						Name:      "mock_tool",
						Arguments: `{"name": "jack"}`,
					},
				},
			},
		}, WithToolsNodeOption(WithToolList(mt), WithToolOption(WithAge(10))))
		assert.NoError(t, err)
		assert.Len(t, out, 1)
		assert.JSONEq(t, `{"echo": "jack: 10"}`, out[0].Content)

		outMessages := make([][]*schema.Message, 0)
		outStream, err := r.Stream(ctx, &schema.Message{
			Role: schema.Assistant,
			ToolCalls: []schema.ToolCall{
				{
					ID: toolIDOfUserCompany,
					Function: schema.FunctionCall{
						Name:      "mock_tool",
						Arguments: `{"name": "jack"}`,
					},
				},
			},
		}, WithToolsNodeOption(WithToolList(mt), WithToolOption(WithAge(10))))

		assert.NoError(t, err)

		for {
			msgs, err := outStream.Recv()
			if err == io.EOF {
				break
			}
			assert.NoError(t, err)
			outMessages = append(outMessages, msgs)
		}
		outStream.Close()

		msgs, err := internal.ConcatItems(outMessages)
		assert.NoError(t, err)

		assert.Len(t, msgs, 1)
		assert.JSONEq(t, `{"echo":"jack: 10"}`, msgs[0].Content)
	})

}

func findMsgByToolCallID(msgs []*schema.Message, toolCallID string) *schema.Message {
	for _, msg := range msgs {
		if msg.ToolCallID == toolCallID {
			return msg
		}
	}

	return nil
}

type mockToolOptions struct {
	Age int
}

func WithAge(age int) tool.Option {
	return tool.WrapImplSpecificOptFn(func(o *mockToolOptions) {
		o.Age = age
	})
}

type mockToolRequest struct {
	Name string `json:"name"`
}

type mockToolResponse struct {
	Echo string `json:"echo"`
}

type mockTool struct{}

func (m *mockTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: "mock_tool",
		Desc: "mock tool",
		ParamsOneOf: schema.NewParamsOneOfByParams(
			map[string]*schema.ParameterInfo{
				"name": {
					Type:     "string",
					Desc:     "name",
					Required: true,
				},
			}),
	}, nil
}

func (m *mockTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	opt := tool.GetImplSpecificOptions(&mockToolOptions{}, opts...)

	req := &mockToolRequest{}

	if e := sonic.UnmarshalString(argumentsInJSON, req); e != nil {
		return "", e
	}

	resp := &mockToolResponse{
		Echo: fmt.Sprintf("%v: %v", req.Name, opt.Age),
	}

	return sonic.MarshalString(resp)
}

func (m *mockTool) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) {
	sr, sw := schema.Pipe[string](1)
	go func() {
		defer sw.Close()

		opt := tool.GetImplSpecificOptions(&mockToolOptions{}, opts...)

		req := &mockToolRequest{}

		if e := sonic.UnmarshalString(argumentsInJSON, req); e != nil {
			sw.Send("", e)
			return
		}

		resp := mockToolResponse{
			Echo: fmt.Sprintf("%v: %v", req.Name, opt.Age),
		}

		output, err := sonic.MarshalString(resp)
		if err != nil {
			sw.Send("", err)
			return
		}

		for i := 0; i < len(output); i++ {
			sw.Send(string(output[i]), nil)
		}
	}()

	return sr, nil
}

func TestUnknownTool(t *testing.T) {
	ctx := context.Background()
	tn, err := NewToolNode(ctx, &ToolsNodeConfig{
		Tools: nil,
		UnknownToolsHandler: func(ctx context.Context, name, input string) (string, error) {
			return "unknown", nil
		},
	})
	assert.NoError(t, err)

	input := &schema.Message{
		Role: schema.Assistant,
		ToolCalls: []schema.ToolCall{
			{
				ID: "1",
				Function: schema.FunctionCall{
					Name:      "unknown1",
					Arguments: `arg1`,
				},
			},
			{
				ID: "2",
				Function: schema.FunctionCall{
					Name:      "unknown2",
					Arguments: `arg2`,
				},
			},
		},
	}

	expected := []*schema.Message{
		{
			Role:       schema.Tool,
			Content:    "unknown",
			ToolCallID: "1",
			ToolName:   "unknown1",
		},
		{
			Role:       schema.Tool,
			Content:    "unknown",
			ToolCallID: "2",
			ToolName:   "unknown2",
		},
	}

	result, err := tn.Invoke(ctx, input)
	assert.NoError(t, err)
	assert.Equal(t, expected, result)

	streamResult, err := tn.Stream(ctx, input)
	assert.NoError(t, err)
	result = make([]*schema.Message, 2)
	for {
		chunk, err := streamResult.Recv()
		if err == io.EOF {
			break
		}
		assert.NoError(t, err)
		for i := range chunk {
			if chunk[i] != nil {
				result[i] = chunk[i]
			}
		}
	}
	assert.Equal(t, expected, result)
}

func TestToolRerun(t *testing.T) {
	type myToolRerunState struct {
		In *schema.Message
	}

	schema.Register[myToolRerunState]()

	tc := []schema.ToolCall{
		{
			ID: "3",
			Function: schema.FunctionCall{
				Name:      "tool3",
				Arguments: "input",
			},
		},
		{
			ID: "4",
			Function: schema.FunctionCall{
				Name:      "tool4",
				Arguments: "input",
			},
		},
		{
			ID: "1",
			Function: schema.FunctionCall{
				Name:      "tool1",
				Arguments: "input",
			},
		},
		{
			ID: "2",
			Function: schema.FunctionCall{
				Name:      "tool2",
				Arguments: "input",
			},
		},
	}
	g := NewGraph[*schema.Message, string](WithGenLocalState(func(ctx context.Context) (state *myToolRerunState) {
		return &myToolRerunState{In: &schema.Message{Role: schema.Assistant, ToolCalls: tc}}
	}))
	ctx := context.Background()
	tn, err := NewToolNode(ctx, &ToolsNodeConfig{
		Tools: []tool.BaseTool{&myTool1{}, &myTool2{}, &myTool3{t: t}, &myTool4{t: t}},
	})
	assert.NoError(t, err)
	assert.NoError(t, g.AddToolsNode("tool node", tn))
	assert.NoError(t, g.AddLambdaNode("lambda", InvokableLambda(func(ctx context.Context, input []*schema.Message) (output string, err error) {
		contents := make([]string, len(input))
		for _, m := range input {
			callID := m.ToolCallID
			callIDInt, err := strconv.Atoi(callID)
			if err != nil {
				return "", err
			}
			contents[callIDInt-1] = m.Content
		}
		sb := strings.Builder{}
		for _, m := range contents {
			sb.WriteString(m)
		}
		return sb.String(), nil
	})))
	assert.NoError(t, g.AddEdge(START, "tool node"))
	assert.NoError(t, g.AddEdge("tool node", "lambda"))
	assert.NoError(t, g.AddEdge("lambda", END))

	r, err := g.Compile(ctx, WithCheckPointStore(&inMemoryStore{m: map[string][]byte{}}))
	assert.NoError(t, err)

	_, err = r.Stream(ctx, &schema.Message{Role: schema.Assistant, ToolCalls: tc}, WithCheckPointID("1"))
	info, ok := ExtractInterruptInfo(err)
	assert.True(t, ok)
	assert.Equal(t, []string{"tool node"}, info.RerunNodes)
	assert.Equal(t, &ToolsInterruptAndRerunExtra{
		ToolCalls:     tc,
		RerunTools:    []string{"1", "2"},
		RerunExtraMap: map[string]any{"1": "tool1 rerun extra", "2": "tool2 rerun extra"},
		ExecutedTools: map[string]string{
			"3": "tool3 input: input",
			"4": "tool4 input: input",
		},
		ExecutedEnhancedTools: make(map[string]*schema.ToolResult),
	}, info.RerunNodesExtra["tool node"])

	sr, err := r.Stream(ctx, nil, WithCheckPointID("1"))
	assert.NoError(t, err)
	result, err := concatStreamReader(sr)
	assert.NoError(t, err)
	assert.Equal(t, "tool1 input: inputtool2 input: inputtool3 input: inputtool4 input: input", result)
}

func TestToolMiddleware(t *testing.T) {
	ctx := context.Background()
	t3 := &myTool3{t: t}
	t4 := &myTool4{t: t}
	tn, err := NewToolNode(ctx, &ToolsNodeConfig{
		Tools: []tool.BaseTool{t3, t4},
		ToolCallMiddlewares: []ToolMiddleware{
			{
				Invokable: func(endpoint InvokableToolEndpoint) InvokableToolEndpoint {
					return func(ctx context.Context, input *ToolInput) (*ToolOutput, error) {
						_, err := endpoint(ctx, input)
						if err != nil {
							return nil, err
						}
						return &ToolOutput{Result: "middleware1"}, nil
					}
				},
			},
			{
				Streamable: func(endpoint StreamableToolEndpoint) StreamableToolEndpoint {
					return func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error) {
						_, err := endpoint(ctx, input)
						if err != nil {
							return nil, err
						}
						return &StreamToolOutput{Result: schema.StreamReaderFromArray([]string{"middleware2"})}, nil
					}
				},
			},
		},
	})
	assert.NoError(t, err)

	messages, err := tn.Invoke(ctx, schema.AssistantMessage("", []schema.ToolCall{
		{ID: "1", Function: schema.FunctionCall{Name: "tool3", Arguments: ""}},
		{ID: "2", Function: schema.FunctionCall{Name: "tool4", Arguments: ""}},
	}))
	assert.NoError(t, err)
	assert.Len(t, messages, 2)
	assert.Equal(t, "middleware1", messages[0].Content)
	assert.Equal(t, "middleware2", messages[1].Content)

	t3.times, t4.times = 0, 0 // reset t3 t4
	messageStreams, err := tn.Stream(ctx, schema.AssistantMessage("", []schema.ToolCall{
		{ID: "1", Function: schema.FunctionCall{Name: "tool3", Arguments: ""}},
		{ID: "2", Function: schema.FunctionCall{Name: "tool4", Arguments: ""}},
	}))
	assert.NoError(t, err)
	var messageArray [][]*schema.Message
	for {
		chunk, err := messageStreams.Recv()
		if err == io.EOF {
			break
		}
		assert.NoError(t, err)
		messageArray = append(messageArray, chunk)
	}
	messages, err = schema.ConcatMessageArray(messageArray)
	assert.Len(t, messages, 2)
	assert.Equal(t, "middleware1", messages[0].Content)
	assert.Equal(t, "middleware2", messages[1].Content)
}

type myTool1 struct {
	times uint
}

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

func (m *myTool1) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	if m.times == 0 {
		m.times++
		return "", tool.Interrupt(ctx, "tool1 rerun extra")
	}
	return "tool1 input: " + argumentsInJSON, nil
}

type myTool2 struct {
	times uint
}

func (m *myTool2) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: "tool2"}, nil
}

func (m *myTool2) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) {
	if m.times == 0 {
		m.times++
		return nil, tool.Interrupt(ctx, "tool2 rerun extra")
	}
	return schema.StreamReaderFromArray([]string{"tool2 input: ", argumentsInJSON}), nil
}

type myTool3 struct {
	t     *testing.T
	times int
}

func (m *myTool3) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: "tool3"}, nil
}

func (m *myTool3) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	assert.Equal(m.t, 0, m.times)
	m.times++
	return "tool3 input: " + argumentsInJSON, nil
}

type myTool4 struct {
	t     *testing.T
	times int
}

func (m *myTool4) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: "tool4"}, nil
}

func (m *myTool4) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) {
	assert.Equal(m.t, 0, m.times)
	m.times++
	return schema.StreamReaderFromArray([]string{"tool4 input: ", argumentsInJSON}), nil
}

func newTool[I, O any](info *schema.ToolInfo, f func(ctx context.Context, in I) (O, error)) tool.InvokableTool {
	return &invokableTool[I, O]{
		info: info,
		fn:   f,
	}
}

type invokableTool[I, O any] struct {
	info *schema.ToolInfo
	fn   func(ctx context.Context, in I) (O, error)
}

func (f *invokableTool[I, O]) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return f.info, nil
}

func (f *invokableTool[I, O]) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
	t := generic.NewInstance[I]()
	err := sonic.UnmarshalString(argumentsInJSON, t)
	if err != nil {
		return "", err
	}
	o, err := f.fn(ctx, t)
	if err != nil {
		return "", err
	}
	return sonic.MarshalString(o)
}

func newStreamableTool[I, O any](info *schema.ToolInfo, f func(ctx context.Context, in I) (*schema.StreamReader[O], error)) tool.StreamableTool {
	return &streamableTool[I, O]{
		info: info,
		fn:   f,
	}
}

type streamableTool[I, O any] struct {
	info *schema.ToolInfo
	fn   func(ctx context.Context, in I) (*schema.StreamReader[O], error)
}

func (f *streamableTool[I, O]) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return f.info, nil
}
func (f *streamableTool[I, O]) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) {
	t := generic.NewInstance[I]()
	err := sonic.UnmarshalString(argumentsInJSON, t)
	if err != nil {
		return nil, err
	}
	sr, err := f.fn(ctx, t)
	if err != nil {
		return nil, err
	}
	return schema.StreamReaderWithConvert(sr, func(o O) (string, error) {
		return sonic.MarshalString(o)
	}), nil
}

type enhancedInvokableTool struct {
	info *schema.ToolInfo
	fn   func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error)
}

func (e *enhancedInvokableTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return e.info, nil
}

func (e *enhancedInvokableTool) InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) {
	return e.fn(ctx, toolArgument)
}

type enhancedStreamableTool struct {
	info *schema.ToolInfo
	fn   func(ctx context.Context, input *schema.ToolArgument) (*schema.StreamReader[*schema.ToolResult], error)
}

func (e *enhancedStreamableTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return e.info, nil
}

func (e *enhancedStreamableTool) StreamableRun(ctx context.Context, toolArgument *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
	return e.fn(ctx, toolArgument)
}

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

	enhancedInvokable := &enhancedInvokableTool{
		info: &schema.ToolInfo{
			Name: "enhanced_invokable_tool",
			Desc: "test enhanced invokable tool",
		},
		fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) {
			return &schema.ToolResult{
				Parts: []schema.ToolOutputPart{
					{Type: schema.ToolPartTypeText, Text: "invokable result: " + input.Text},
				},
			}, nil
		},
	}

	enhancedStreamable := &enhancedStreamableTool{
		info: &schema.ToolInfo{
			Name: "enhanced_streamable_tool",
			Desc: "test enhanced streamable tool",
		},
		fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.StreamReader[*schema.ToolResult], error) {
			results := []*schema.ToolResult{
				{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "stream part 1: " + input.Text}}},
				{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: " stream part 2"}}},
			}
			return schema.StreamReaderFromArray(results), nil
		},
	}

	toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{
		Tools: []tool.BaseTool{enhancedInvokable, enhancedStreamable},
	})
	assert.NoError(t, err)
	assert.NotNil(t, toolNode)

	t.Run("enhanced invokable tool", func(t *testing.T) {
		input := schema.AssistantMessage("", []schema.ToolCall{
			{
				ID: "call1",
				Function: schema.FunctionCall{
					Name:      "enhanced_invokable_tool",
					Arguments: "test input",
				},
			},
		})

		output, err := toolNode.Invoke(ctx, input)
		assert.NoError(t, err)
		assert.Len(t, output, 1)
		assert.Equal(t, schema.Tool, output[0].Role)
		assert.Equal(t, "call1", output[0].ToolCallID)
	})

	t.Run("enhanced streamable tool", func(t *testing.T) {
		input := schema.AssistantMessage("", []schema.ToolCall{
			{
				ID: "call2",
				Function: schema.FunctionCall{
					Name:      "enhanced_streamable_tool",
					Arguments: "test stream",
				},
			},
		})

		streamReader, err := toolNode.Stream(ctx, input)
		assert.NoError(t, err)
		assert.NotNil(t, streamReader)

		var messages []*schema.Message
		for {
			chunk, err := streamReader.Recv()
			if err != nil {
				break
			}
			if chunk != nil {
				messages = append(messages, chunk...)
			}
		}
		message, err := schema.ConcatMessages(messages)
		assert.NoError(t, err)
		assert.Len(t, messages, 2)
		assert.Equal(t, schema.Tool, messages[0].Role)
		assert.Equal(t, "call2", messages[0].ToolCallID)
		assert.Contains(t, message.UserInputMultiContent[0].Text, "stream part")
	})
}

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

	enhancedInvokable := &enhancedInvokableTool{
		info: &schema.ToolInfo{
			Name: "enhanced_only_invokable",
			Desc: "test enhanced invokable only",
		},
		fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) {
			return &schema.ToolResult{
				Parts: []schema.ToolOutputPart{
					{Type: schema.ToolPartTypeText, Text: "enhanced: " + input.Text},
				},
			}, nil
		},
	}

	toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{
		Tools: []tool.BaseTool{enhancedInvokable},
	})
	assert.NoError(t, err)

	t.Run("enhanced invokable auto-converts to streamable", func(t *testing.T) {
		input := schema.AssistantMessage("", []schema.ToolCall{
			{
				ID: "call1",
				Function: schema.FunctionCall{
					Name:      "enhanced_only_invokable",
					Arguments: "test",
				},
			},
		})

		streamReader, err := toolNode.Stream(ctx, input)
		assert.NoError(t, err)
		assert.NotNil(t, streamReader)

		var messages []*schema.Message
		for {
			chunk, err := streamReader.Recv()
			if err != nil {
				break
			}
			if chunk != nil {
				messages = append(messages, chunk...)
			}
		}
		assert.Len(t, messages, 1)
	})
}

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

	var invokableMiddlewareCalled bool
	var streamableMiddlewareCalled bool

	enhancedInvokable := &enhancedInvokableTool{
		info: &schema.ToolInfo{
			Name: "enhanced_tool_with_middleware",
			Desc: "test enhanced tool with middleware",
		},
		fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) {
			return &schema.ToolResult{
				Parts: []schema.ToolOutputPart{
					{Text: "result", Type: schema.ToolPartTypeText},
				},
			}, nil
		},
	}

	toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{
		Tools: []tool.BaseTool{enhancedInvokable},
		ToolCallMiddlewares: []ToolMiddleware{
			{
				EnhancedInvokable: func(next EnhancedInvokableToolEndpoint) EnhancedInvokableToolEndpoint {
					return func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error) {
						invokableMiddlewareCalled = true
						return next(ctx, input)
					}
				},
				EnhancedStreamable: func(next EnhancedStreamableToolEndpoint) EnhancedStreamableToolEndpoint {
					return func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error) {
						streamableMiddlewareCalled = true
						return next(ctx, input)
					}
				},
			},
		},
	})
	assert.NoError(t, err)

	t.Run("enhanced invokable middleware", func(t *testing.T) {
		invokableMiddlewareCalled = false
		input := schema.AssistantMessage("", []schema.ToolCall{
			{
				ID: "call1",
				Function: schema.FunctionCall{
					Name:      "enhanced_tool_with_middleware",
					Arguments: "test",
				},
			},
		})

		_, err := toolNode.Invoke(ctx, input)
		assert.NoError(t, err)
		assert.True(t, invokableMiddlewareCalled)
	})

	t.Run("enhanced streamable middleware", func(t *testing.T) {
		streamableMiddlewareCalled = false
		input := schema.AssistantMessage("", []schema.ToolCall{
			{
				ID: "call2",
				Function: schema.FunctionCall{
					Name:      "enhanced_tool_with_middleware",
					Arguments: "test",
				},
			},
		})

		streamReader, err := toolNode.Stream(ctx, input)
		assert.NoError(t, err)
		for {
			_, err := streamReader.Recv()
			if err != nil {
				break
			}
		}
		assert.False(t, streamableMiddlewareCalled)
	})
}

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

	enhancedInvokable := &enhancedInvokableTool{
		info: &schema.ToolInfo{
			Name: "test_tool",
			Desc: "test tool with both enhanced and regular",
		},
		fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) {
			return &schema.ToolResult{
				Parts: []schema.ToolOutputPart{
					{Text: "enhanced result", Type: schema.ToolPartTypeText},
				},
			}, nil
		},
	}

	toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{
		Tools: []tool.BaseTool{enhancedInvokable},
	})
	assert.NoError(t, err)

	t.Run("enhanced tool is used when available", func(t *testing.T) {
		input := schema.AssistantMessage("", []schema.ToolCall{
			{
				ID: "call1",
				Function: schema.FunctionCall{
					Name:      "test_tool",
					Arguments: "test",
				},
			},
		})

		output, err := toolNode.Invoke(ctx, input)
		assert.NoError(t, err)
		assert.Len(t, output, 1)
		assert.Contains(t, output[0].UserInputMultiContent[0].Text, "enhanced result")
	})
}
