/*
 * 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"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

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

type mockChatModelForAttack struct {
	generateFn func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error)
}

func (m *mockChatModelForAttack) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
	return m.generateFn(ctx, input, opts...)
}

func (m *mockChatModelForAttack) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	result, err := m.generateFn(ctx, input, opts...)
	if err != nil {
		return nil, err
	}
	r, w := schema.Pipe[*schema.Message](1)
	go func() { defer w.Close(); w.Send(result, nil) }()
	return r, nil
}

// mockAgent implements the Agent interface for testing
type mockAgentForTool struct {
	name        string
	description string
	responses   []*AgentEvent
}

func (a *mockAgentForTool) Name(_ context.Context) string {
	return a.name
}

func (a *mockAgentForTool) Description(_ context.Context) string {
	return a.description
}

func (a *mockAgentForTool) Run(_ context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	iterator, generator := NewAsyncIteratorPair[*AgentEvent]()

	go func() {
		defer generator.Close()

		for _, event := range a.responses {
			generator.Send(event)

			// If the event has an Exit action, stop sending events
			if event.Action != nil && event.Action.Exit {
				break
			}
		}
	}()

	return iterator
}

func newMockAgentForTool(name, description string, responses []*AgentEvent) *mockAgentForTool {
	return &mockAgentForTool{
		name:        name,
		description: description,
		responses:   responses,
	}
}

func TestAgentTool_Info(t *testing.T) {
	// Create a mock agent
	mockAgent_ := newMockAgentForTool("TestAgent", "Test agent description", nil)

	// Create an agentTool with the mock agent
	agentTool_ := NewAgentTool(context.Background(), mockAgent_)

	// Test the Info method
	ctx := context.Background()
	info, err := agentTool_.Info(ctx)

	// Verify results
	assert.NoError(t, err)
	assert.NotNil(t, info)
	assert.Equal(t, "TestAgent", info.Name)
	assert.Equal(t, "Test agent description", info.Desc)
	assert.NotNil(t, info.ParamsOneOf)
}

func TestAgentTool_Info_EmptyName(t *testing.T) {
	agentTool_ := NewAgentTool(context.Background(), newMockAgentForTool("", "desc", nil))
	_, err := agentTool_.Info(context.Background())
	assert.ErrorContains(t, err, "non-empty Name")
}

func TestAgentTool_Info_EmptyDescription(t *testing.T) {
	agentTool_ := NewAgentTool(context.Background(), newMockAgentForTool("name", "", nil))
	_, err := agentTool_.Info(context.Background())
	assert.ErrorContains(t, err, "non-empty Description")
}

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

	inner := &sessionValuesAgent{name: "inner"}
	innerTool := NewAgentTool(ctx, inner).(tool.InvokableTool)

	input := &AgentInput{Messages: []Message{schema.UserMessage("q")}}
	ctx, _ = initRunCtx(ctx, "outer", input)
	AddSessionValue(ctx, "parent_key", "parent_val")
	parentSession := getRunCtx(ctx).Session

	_, err := innerTool.InvokableRun(ctx, `{"request":"hello"}`)
	assert.NoError(t, err)

	assert.Equal(t, "parent_val", inner.seenParentValue)
	assert.NotNil(t, inner.capturedSession)
	assert.NotSame(t, parentSession, inner.capturedSession)
	assert.NotNil(t, parentSession.valuesMtx)
	assert.Same(t, parentSession.valuesMtx, inner.capturedSession.valuesMtx)

	mtx := parentSession.valuesMtx
	mtx.Lock()
	inner.capturedSession.Values["direct_child_key"] = "direct_child_val"
	mtx.Unlock()

	mtx.Lock()
	v2, ok2 := parentSession.Values["direct_child_key"]
	mtx.Unlock()
	assert.True(t, ok2)
	assert.Equal(t, "direct_child_val", v2)

	mtx.Lock()
	parentSession.Values["direct_parent_key"] = "direct_parent_val"
	mtx.Unlock()

	mtx.Lock()
	v3, ok3 := inner.capturedSession.Values["direct_parent_key"]
	mtx.Unlock()
	assert.True(t, ok3)
	assert.Equal(t, "direct_parent_val", v3)

	v, ok := GetSessionValue(ctx, "child_key")
	assert.True(t, ok)
	assert.Equal(t, "child_val", v)
}

type sessionValuesAgent struct {
	name            string
	seenParentValue any
	capturedSession *runSession
}

func (a *sessionValuesAgent) Name(context.Context) string        { return a.name }
func (a *sessionValuesAgent) Description(context.Context) string { return "test" }
func (a *sessionValuesAgent) Run(ctx context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	if rc := getRunCtx(ctx); rc != nil {
		a.capturedSession = rc.Session
	}
	a.seenParentValue, _ = GetSessionValue(ctx, "parent_key")
	AddSessionValue(ctx, "child_key", "child_val")

	it, gen := NewAsyncIteratorPair[*AgentEvent]()
	gen.Send(&AgentEvent{
		AgentName: a.name,
		Output: &AgentOutput{
			MessageOutput: &MessageVariant{
				IsStreaming: false,
				Message:     schema.AssistantMessage("ok", nil),
				Role:        schema.Assistant,
			},
		},
	})
	gen.Close()
	return it
}

func TestAgentTool_InvokableRun(t *testing.T) {
	// Create a context
	ctx := context.Background()

	// Test cases
	tests := []struct {
		name           string
		agentResponses []*AgentEvent
		request        string
		expectedOutput string
		expectError    bool
	}{
		{
			name: "successful model response",
			agentResponses: []*AgentEvent{
				{
					AgentName: "TestAgent",
					Output: &AgentOutput{
						MessageOutput: &MessageVariant{
							IsStreaming: false,
							Message:     schema.AssistantMessage("Test response", nil),
							Role:        schema.Assistant,
						},
					},
				},
			},
			request:        `{"request":"Test request"}`,
			expectedOutput: "Test response",
			expectError:    false,
		},
		{
			name: "successful tool call response",
			agentResponses: []*AgentEvent{
				{
					AgentName: "TestAgent",
					Output: &AgentOutput{
						MessageOutput: &MessageVariant{
							IsStreaming: false,
							Message:     schema.ToolMessage("Tool response", "test-id"),
							Role:        schema.Tool,
						},
					},
				},
			},
			request:        `{"request":"Test tool request"}`,
			expectedOutput: "Tool response",
			expectError:    false,
		},
		{
			name:           "invalid request JSON",
			agentResponses: nil,
			request:        `invalid json`,
			expectedOutput: "",
			expectError:    true,
		},
		{
			name:           "no events returned",
			agentResponses: []*AgentEvent{},
			request:        `{"request":"Test request"}`,
			expectedOutput: "",
			expectError:    true,
		},
		{
			name: "error in event",
			agentResponses: []*AgentEvent{
				{
					AgentName: "TestAgent",
					Err:       assert.AnError,
				},
			},
			request:        `{"request":"Test request"}`,
			expectedOutput: "",
			expectError:    true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create a mock agent with the test responses
			mockAgent_ := newMockAgentForTool("TestAgent", "Test agent description", tt.agentResponses)

			// Create an agentTool with the mock agent
			agentTool_ := NewAgentTool(ctx, mockAgent_)

			// Call InvokableRun
			output, err := agentTool_.(tool.InvokableTool).InvokableRun(ctx, tt.request)

			// Verify results
			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expectedOutput, output)
			}
		})
	}
}

func TestGetReactHistory(t *testing.T) {
	g := compose.NewGraph[string, []Message](compose.WithGenLocalState(func(ctx context.Context) (state *State) {
		return &State{
			Messages: []Message{
				schema.UserMessage("user query"),
				schema.AssistantMessage("", []schema.ToolCall{{ID: "tool call id 1", Function: schema.FunctionCall{Name: "tool1", Arguments: "arguments1"}}}),
				schema.ToolMessage("tool result 1", "tool call id 1", schema.WithToolName("tool1")),
				schema.AssistantMessage("", []schema.ToolCall{{ID: "tool call id 2", Function: schema.FunctionCall{Name: "tool2", Arguments: "arguments2"}}}),
			},
		}
	}))
	assert.NoError(t, g.AddLambdaNode("1", compose.InvokableLambda(func(ctx context.Context, input string) (output []Message, err error) {
		return getReactChatHistory(ctx, "DestAgentName")
	})))
	assert.NoError(t, g.AddEdge(compose.START, "1"))
	assert.NoError(t, g.AddEdge("1", compose.END))

	ctx := context.Background()
	ctx, _ = initRunCtx(ctx, "MyAgent", nil)
	runner, err := g.Compile(ctx)
	assert.NoError(t, err)
	result, err := runner.Invoke(ctx, "")
	assert.NoError(t, err)
	assert.Equal(t, []Message{
		schema.UserMessage("user query"),
		schema.UserMessage("For context: [MyAgent] called tool: `tool1` with arguments: arguments1."),
		schema.UserMessage("For context: [MyAgent] `tool1` tool returned result: tool result 1."),
		schema.UserMessage("For context: [MyAgent] called tool: `transfer_to_agent` with arguments: DestAgentName."),
		schema.UserMessage("For context: [MyAgent] `transfer_to_agent` tool returned result: successfully transferred to agent [DestAgentName]."),
	}, result)
}

// mockAgentWithInputCapture implements the Agent interface for testing and captures the input it receives
type mockAgentWithInputCapture struct {
	name          string
	description   string
	capturedInput []Message
	responses     []*AgentEvent
}

func (a *mockAgentWithInputCapture) Name(_ context.Context) string {
	return a.name
}

func (a *mockAgentWithInputCapture) Description(_ context.Context) string {
	return a.description
}

func (a *mockAgentWithInputCapture) Run(_ context.Context, input *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	a.capturedInput = input.Messages

	iterator, generator := NewAsyncIteratorPair[*AgentEvent]()

	go func() {
		defer generator.Close()

		for _, event := range a.responses {
			generator.Send(event)

			// If the event has an Exit action, stop sending events
			if event.Action != nil && event.Action.Exit {
				break
			}
		}
	}()

	return iterator
}

func newMockAgentWithInputCapture(name, description string, responses []*AgentEvent) *mockAgentWithInputCapture {
	return &mockAgentWithInputCapture{
		name:        name,
		description: description,
		responses:   responses,
	}
}

func TestAgentToolWithOptions(t *testing.T) {
	// Test Case 1: WithFullChatHistoryAsInput
	t.Run("WithFullChatHistoryAsInput", func(t *testing.T) {
		ctx := context.Background()

		// 1. Set up a mock agent that will capture the input it receives
		mockAgent := newMockAgentWithInputCapture("test-agent", "a test agent", []*AgentEvent{
			{
				AgentName: "test-agent",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						IsStreaming: false,
						Message:     schema.AssistantMessage("done", nil),
						Role:        schema.Assistant,
					},
				},
			},
		})

		// 2. Create an agentTool with the option
		agentTool := NewAgentTool(ctx, mockAgent, WithFullChatHistoryAsInput())

		// 3. Set up a context with a chat history using a graph
		history := []Message{
			schema.UserMessage("first user message"),
			schema.AssistantMessage("first assistant response", nil),
		}

		g := compose.NewGraph[string, string](compose.WithGenLocalState(func(ctx context.Context) (state *State) {
			return &State{
				Messages: append(history, schema.AssistantMessage("tool call msg", nil)),
			}
		}))

		assert.NoError(t, g.AddLambdaNode("1", compose.InvokableLambda(func(ctx context.Context, input string) (output string, err error) {
			// Run the tool within the graph context that has the state
			_, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"request":"some ignored input"}`)
			return "done", err
		})))
		assert.NoError(t, g.AddEdge(compose.START, "1"))
		assert.NoError(t, g.AddEdge("1", compose.END))

		ctx, _ = initRunCtx(ctx, "react-agent", nil)
		runner, err := g.Compile(ctx)
		assert.NoError(t, err)

		// 4. Run the graph which will execute the tool with the state
		_, err = runner.Invoke(ctx, "")
		assert.NoError(t, err)

		// 5. Assert that the agent received the full history
		// The agent should receive: history (minus last assistant message) + transfer messages
		assert.Len(t, mockAgent.capturedInput, 4) // 2 from history + 2 transfer messages
		assert.Equal(t, "first user message", mockAgent.capturedInput[0].Content)
		assert.Equal(t, "For context: [react-agent] said: first assistant response.", mockAgent.capturedInput[1].Content)
		assert.Equal(t, "For context: [react-agent] called tool: `transfer_to_agent` with arguments: test-agent.", mockAgent.capturedInput[2].Content)
		assert.Equal(t, "For context: [react-agent] `transfer_to_agent` tool returned result: successfully transferred to agent [test-agent].", mockAgent.capturedInput[3].Content)
	})

	// Test Case 2: WithAgentInputSchema
	t.Run("WithAgentInputSchema", func(t *testing.T) {
		ctx := context.Background()

		// 1. Define a custom schema
		customSchema := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
			"custom_arg": {
				Desc:     "a custom argument",
				Required: true,
				Type:     schema.String,
			},
		})

		// 2. Set up a mock agent to capture input
		mockAgent := newMockAgentWithInputCapture("schema-agent", "agent with custom schema", []*AgentEvent{
			{
				AgentName: "schema-agent",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						IsStreaming: false,
						Message:     schema.AssistantMessage("schema processed", nil),
						Role:        schema.Assistant,
					},
				},
			},
		})

		// 3. Create agentTool with the custom schema option
		agentTool := NewAgentTool(ctx, mockAgent, WithAgentInputSchema(customSchema))

		// 4. Verify the Info() method returns the custom schema
		info, err := agentTool.Info(ctx)
		assert.NoError(t, err)
		assert.Equal(t, customSchema, info.ParamsOneOf)

		// 5. Run the tool with arguments matching the custom schema
		_, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"custom_arg":"hello world"}`)
		assert.NoError(t, err)

		// 6. Assert that the agent received the correctly parsed argument
		// With custom schema, the agent should receive the raw JSON as input
		assert.Len(t, mockAgent.capturedInput, 1)
		assert.Equal(t, `{"custom_arg":"hello world"}`, mockAgent.capturedInput[0].Content)
	})

	// Test Case 3: WithAgentInputSchema with complex schema
	t.Run("WithAgentInputSchema_ComplexSchema", func(t *testing.T) {
		ctx := context.Background()

		// 1. Define a complex custom schema with multiple parameters
		complexSchema := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
			"name": {
				Desc:     "user name",
				Required: true,
				Type:     schema.String,
			},
			"age": {
				Desc:     "user age",
				Required: false,
				Type:     schema.Integer,
			},
			"active": {
				Desc:     "user status",
				Required: false,
				Type:     schema.Boolean,
			},
		})

		// 2. Set up a mock agent
		mockAgent := newMockAgentWithInputCapture("complex-agent", "agent with complex schema", []*AgentEvent{
			{
				AgentName: "complex-agent",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						IsStreaming: false,
						Message:     schema.AssistantMessage("complex processed", nil),
						Role:        schema.Assistant,
					},
				},
			},
		})

		// 3. Create agentTool with the complex schema option
		agentTool := NewAgentTool(ctx, mockAgent, WithAgentInputSchema(complexSchema))

		// 4. Verify the Info() method returns the complex schema
		info, err := agentTool.Info(ctx)
		assert.NoError(t, err)
		assert.Equal(t, complexSchema, info.ParamsOneOf)

		// 5. Run the tool with complex arguments
		_, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"name":"John","age":30,"active":true}`)
		assert.NoError(t, err)

		// 6. Assert that the agent received the complex JSON
		assert.Len(t, mockAgent.capturedInput, 1)
		assert.Equal(t, `{"name":"John","age":30,"active":true}`, mockAgent.capturedInput[0].Content)
	})

	// Test Case 4: Both options together
	t.Run("BothOptionsTogether", func(t *testing.T) {
		ctx := context.Background()

		// 1. Define a custom schema
		customSchema := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
			"query": {
				Desc:     "search query",
				Required: true,
				Type:     schema.String,
			},
		})

		// 2. Set up a mock agent
		mockAgent := newMockAgentWithInputCapture("combined-agent", "agent with both options", []*AgentEvent{
			{
				AgentName: "combined-agent",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						IsStreaming: false,
						Message:     schema.AssistantMessage("combined processed", nil),
						Role:        schema.Assistant,
					},
				},
			},
		})

		// 3. Create agentTool with both options
		agentTool := NewAgentTool(ctx, mockAgent, WithAgentInputSchema(customSchema), WithFullChatHistoryAsInput())

		// 4. Set up a context with chat history using a graph
		history := []Message{
			schema.UserMessage("previous conversation"),
			schema.AssistantMessage("previous response", nil),
		}

		g := compose.NewGraph[string, string](compose.WithGenLocalState(func(ctx context.Context) (state *State) {
			return &State{
				Messages: append(history, schema.AssistantMessage("tool call", nil)),
			}
		}))

		assert.NoError(t, g.AddLambdaNode("1", compose.InvokableLambda(func(ctx context.Context, input string) (output string, err error) {
			// Run the tool within the graph context that has the state
			_, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"query":"current query"}`)
			return "done", err
		})))
		assert.NoError(t, g.AddEdge(compose.START, "1"))
		assert.NoError(t, g.AddEdge("1", compose.END))

		ctx, _ = initRunCtx(ctx, "react-agent", nil)
		runner, err := g.Compile(ctx)
		assert.NoError(t, err)

		// 5. Run the graph which will execute the tool with the state
		_, err = runner.Invoke(ctx, "")
		assert.NoError(t, err)

		// 6. Verify both options work together
		info, err := agentTool.Info(ctx)
		assert.NoError(t, err)
		assert.Equal(t, customSchema, info.ParamsOneOf)

		// The agent should receive full history + the custom query
		assert.Len(t, mockAgent.capturedInput, 4) // 2 history + 2 transfer messages
		assert.Equal(t, "previous conversation", mockAgent.capturedInput[0].Content)
		assert.Equal(t, "For context: [react-agent] said: previous response.", mockAgent.capturedInput[1].Content)
		assert.Equal(t, "For context: [react-agent] called tool: `transfer_to_agent` with arguments: combined-agent.", mockAgent.capturedInput[2].Content)
		assert.Equal(t, "For context: [react-agent] `transfer_to_agent` tool returned result: successfully transferred to agent [combined-agent].", mockAgent.capturedInput[3].Content)
	})
}

type fakeTCM struct{}

func (f *fakeTCM) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
	o := model.GetCommonOptions(&model.Options{}, opts...)
	tc := schema.ToolCall{ID: "id-1", Type: "function"}
	if len(o.Tools) > 0 {
		tc.Function.Name = o.Tools[0].Name
	}
	tc.Function.Arguments = `{"request":"hello"}`
	return schema.AssistantMessage("", []schema.ToolCall{tc}), nil
}
func (f *fakeTCM) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	msg, _ := f.Generate(ctx, input, opts...)
	return schema.StreamReaderFromArray([]*schema.Message{msg}), nil
}
func (f *fakeTCM) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
	return f, nil
}

type emitOnceModel struct{}

func (e *emitOnceModel) Generate(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) {
	return schema.AssistantMessage("inner2", nil), nil
}
func (e *emitOnceModel) Stream(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	m, _ := e.Generate(ctx, input)
	return schema.StreamReaderFromArray([]*schema.Message{m}), nil
}
func (e *emitOnceModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
	return e, nil
}

type emitEventsAgent struct{ events []*AgentEvent }

func (e *emitEventsAgent) Name(context.Context) string        { return "emit" }
func (e *emitEventsAgent) Description(context.Context) string { return "test" }
func (e *emitEventsAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	it, gen := NewAsyncIteratorPair[*AgentEvent]()
	go func() {
		for _, ev := range e.events {
			gen.Send(ev)
		}
		gen.Close()
	}()
	return it
}

// spyAgent captures runSession from ctx in a single nested run
type spyAgent struct {
	a        Agent
	mu       sync.Mutex
	captured *runSession
}

func (s *spyAgent) Name(ctx context.Context) string        { return s.a.Name(ctx) }
func (s *spyAgent) Description(ctx context.Context) string { return s.a.Description(ctx) }
func (s *spyAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	if rc := getRunCtx(ctx); rc != nil {
		s.mu.Lock()
		s.captured = rc.Session
		s.mu.Unlock()
	}
	return s.a.Run(ctx, input, options...)
}

func (s *spyAgent) getCaptured() *runSession {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.captured
}

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

	inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner2",
		Description: "leaf",
		Model:       &emitOnceModel{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: true},
	})
	inner2Spy := &spyAgent{a: inner2}
	inner2Tool := NewAgentTool(ctx, inner2Spy)

	inner, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner",
		Description: "mid",
		Model:       &fakeTCM{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: true, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{inner2Tool}}},
	})
	innerSpy := &spyAgent{a: inner}
	innerTool := NewAgentTool(ctx, innerSpy)

	outer, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "outer",
		Description: "top",
		Model:       &fakeTCM{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: true, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerTool}}},
	})

	input := &AgentInput{Messages: []Message{schema.UserMessage("q")}}
	ctx, outerRunCtx := initRunCtx(ctx, "outer", input)
	r := NewRunner(ctx, RunnerConfig{Agent: outer, EnableStreaming: false, CheckPointStore: newBridgeStore()})
	it := r.Run(ctx, []Message{schema.UserMessage("q")})

	var target *AgentEvent
	for {
		ev, ok := it.Next()
		if !ok {
			break
		}
		if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming {
			if ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Content == "inner2" {
				target = ev
				break
			}
		}
	}
	if target == nil {
		t.Fatalf("no inner2 event found in ephemerals")
	}

	got := make([]string, len(target.RunPath))
	for i := range target.RunPath {
		got[i] = target.RunPath[i].agentName
	}
	want := []string{"outer", "inner", "inner2"}
	if len(got) != len(want) {
		t.Fatalf("unexpected runPath len: got %d want %d: %+v", len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("runPath mismatch at %d: got %s want %s; full: %+v", i, got[i], want[i], got)
		}
	}

	for _, w := range outerRunCtx.Session.getEvents() {
		if w.AgentName != "outer" {
			t.Fatalf("outer session contains non-outer event: %s", w.AgentName)
		}
	}
	if innerSpy.getCaptured() == nil {
		t.Fatalf("inner spy did not capture session")
	}
	for _, w := range innerSpy.getCaptured().getEvents() {
		if w.AgentName != "inner" {
			t.Fatalf("inner session contains non-inner event: %s", w.AgentName)
		}
	}
	if inner2Spy.getCaptured() == nil {
		t.Fatalf("inner2 spy did not capture session")
	}
	for _, w := range inner2Spy.getCaptured().getEvents() {
		if w.AgentName != "inner2" {
			t.Fatalf("inner2 session contains non-inner2 event: %s", w.AgentName)
		}
	}
}

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

	inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner2",
		Description: "leaf",
		Model:       &emitOnceModel{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: false},
	})
	inner2Tool := NewAgentTool(ctx, inner2)

	inner, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner",
		Description: "mid",
		Model:       &fakeTCM{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: false, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{inner2Tool}}},
	})
	innerTool := NewAgentTool(ctx, inner)

	outer, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "outer",
		Description: "top",
		Model:       &fakeTCM{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: false, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerTool}}},
	})

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

	for {
		ev, ok := it.Next()
		if !ok {
			break
		}
		if ev.AgentName == "inner2" {
			t.Fatalf("inner2 internal event should not be emitted when disabled")
		}
	}
}

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

	innerTool := &simpleTool{name: "inner_tool", result: "inner_tool_result"}
	inner, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner",
		Description: "inner agent with tool",
		Model:       &fakeTCM{},
		ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerTool}}},
	})
	innerAgentTool := NewAgentTool(ctx, inner)

	outer, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "outer",
		Description: "outer agent",
		Model:       &fakeTCM{},
		ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerAgentTool}}},
	})

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

	var allEvents []*AgentEvent
	for {
		ev, ok := it.Next()
		if !ok {
			break
		}
		allEvents = append(allEvents, ev)
	}

	for _, ev := range allEvents {
		if ev.Output != nil && ev.Output.MessageOutput != nil &&
			ev.Output.MessageOutput.Message != nil &&
			ev.Output.MessageOutput.Message.Role == schema.Tool &&
			ev.AgentName == "outer" &&
			ev.Output.MessageOutput.Message.Content == "inner_tool_result" {
			t.Fatalf("inner agent's tool result (inner_tool_result) should not be emitted as outer agent's event, but got event with AgentName=%s, Content=%s",
				ev.AgentName, ev.Output.MessageOutput.Message.Content)
		}
	}
}

type simpleTool struct {
	name   string
	result string
}

func (s *simpleTool) Info(context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{Name: s.name, Desc: "simple tool"}, nil
}

func (s *simpleTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	return s.result, nil
}

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

	interrupted := &AgentEvent{AgentName: "TestAgent"}
	interrupted.Action = StatefulInterrupt(ctx, "info", "state").Action

	err := compositeInterruptFromLast(ctx, &bridgeStore{}, interrupted)
	if err == nil {
		t.Fatalf("expected error for interrupt without checkpoint")
	}
	if !strings.Contains(err.Error(), "interrupt occurred but checkpoint data is missing") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func compositeInterruptFromLast(ctx context.Context, ms *bridgeStore, lastEvent *AgentEvent) error {
	if lastEvent == nil || lastEvent.Action == nil || lastEvent.Action.Interrupted == nil {
		return nil
	}
	data, existed, err := ms.Get(ctx, bridgeCheckpointID)
	if err != nil {
		return fmt.Errorf("failed to get interrupt info: %w", err)
	}
	if !existed {
		return fmt.Errorf("interrupt occurred but checkpoint data is missing")
	}
	return tool.CompositeInterrupt(ctx, "agent tool interrupt", data, lastEvent.Action.internalInterrupted)
}

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

	inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner2",
		Description: "leaf",
		Model:       &emitOnceModel{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: true},
	})
	invTool := NewAgentTool(ctx, inner2)
	out, err := invTool.(tool.InvokableTool).InvokableRun(ctx, `{"request":"q"}`)
	if err != nil {
		t.Fatalf("invokable run error: %v", err)
	}
	if out != "inner2" {
		t.Fatalf("unexpected output: %s", out)
	}
}

type streamingAgent struct{}

func (s *streamingAgent) Name(context.Context) string        { return "stream" }
func (s *streamingAgent) Description(context.Context) string { return "test" }
func (s *streamingAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	it, gen := NewAsyncIteratorPair[*AgentEvent]()
	go func() {
		mv := &MessageVariant{IsStreaming: true, MessageStream: schema.StreamReaderFromArray([]Message{schema.AssistantMessage("1", nil), schema.AssistantMessage("2", nil)})}
		gen.Send(&AgentEvent{AgentName: "stream", Output: &AgentOutput{MessageOutput: mv}})
		mv = &MessageVariant{IsStreaming: true, MessageStream: schema.StreamReaderFromArray([]Message{schema.AssistantMessage("a", nil), schema.AssistantMessage("b", nil)})}
		gen.Send(&AgentEvent{AgentName: "stream", Output: &AgentOutput{MessageOutput: mv}})
		gen.Close()
	}()
	return it
}

func TestAgentTool_InvokableRun_StreamingVariant(t *testing.T) {
	ctx := context.Background()
	agent := &streamingAgent{}
	it := NewAgentTool(ctx, agent)
	out, err := it.(tool.InvokableTool).InvokableRun(ctx, `{"request":"q"}`)
	if err != nil {
		t.Fatalf("invokable run error: %v", err)
	}
	if out != "ab" {
		t.Fatalf("unexpected output: %s", out)
	}
}

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

	inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner2",
		Description: "leaf",
		Model:       &emitOnceModel{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: true},
	})
	inner2Spy := &spyAgent{a: inner2}
	inner2ToolSpy := NewAgentTool(ctx, inner2Spy)

	innerWithSpy, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "inner",
		Description: "mid",
		Model:       &fakeTCM{},
		ToolsConfig: ToolsConfig{EmitInternalEvents: true, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{inner2ToolSpy}}},
	})
	innerSpy := &spyAgent{a: innerWithSpy}

	outer, err := NewSequentialAgent(ctx, &SequentialAgentConfig{
		Name:        "outer-seq",
		Description: "workflow",
		SubAgents:   []Agent{innerSpy},
	})
	if err != nil {
		t.Fatalf("new sequential agent err: %v", err)
	}

	input := &AgentInput{Messages: []Message{schema.UserMessage("q")}}
	ctx, outerRunCtx := initRunCtx(ctx, "outer-seq", input)
	r := NewRunner(ctx, RunnerConfig{Agent: outer, EnableStreaming: false, CheckPointStore: newBridgeStore()})
	it := r.Run(ctx, []Message{schema.UserMessage("q")})

	var target *AgentEvent
	for {
		ev, ok := it.Next()
		if !ok {
			break
		}
		if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming {
			if ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Content == "inner2" {
				target = ev
				break
			}
		}
	}
	if target == nil {
		t.Fatalf("no inner2 event found")
	}

	got := make([]string, len(target.RunPath))
	for i := range target.RunPath {
		got[i] = target.RunPath[i].agentName
	}
	want := []string{"outer-seq", "inner", "inner2"}
	if len(got) != len(want) {
		t.Fatalf("unexpected runPath len: got %d want %d: %+v", len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("runPath mismatch at %d: got %s want %s; full: %+v", i, got[i], want[i], got)
		}
	}

	for _, w := range outerRunCtx.Session.getEvents() {
		if w.AgentName != "outer-seq" {
			t.Fatalf("outer session contains non-outer event: %s", w.AgentName)
		}
	}
	if innerSpy.getCaptured() == nil {
		t.Fatalf("inner spy did not capture session")
	}
	for _, w := range innerSpy.getCaptured().getEvents() {
		if w.AgentName != "inner" {
			t.Fatalf("inner session contains non-inner event: %s", w.AgentName)
		}
	}
	if inner2Spy.getCaptured() == nil {
		t.Fatalf("inner2 spy did not capture session")
	}
	for _, w := range inner2Spy.getCaptured().getEvents() {
		if w.AgentName != "inner2" {
			t.Fatalf("inner2 session contains non-inner2 event: %s", w.AgentName)
		}
	}
}

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

	innerExit := &AgentEvent{Action: &AgentAction{Exit: true}, RunPath: []RunStep{{agentName: "inner"}}}
	finalOut := EventFromMessage(schema.AssistantMessage("ok", nil), nil, schema.Assistant, "")

	sub := &emitEventsAgent{events: []*AgentEvent{innerExit, finalOut}}
	fa := toFlowAgent(ctx, sub)

	it := fa.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("q")}})

	var sawFinal bool
	for {
		ev, ok := it.Next()
		if !ok {
			break
		}
		if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming {
			if ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Content == "ok" {
				sawFinal = true
			}
		}
	}
	if !sawFinal {
		t.Fatalf("final output not observed; parent may have exited on inner Exit action")
	}
}

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

	innerTransfer := &AgentEvent{Action: NewTransferToAgentAction("ghost"), RunPath: []RunStep{{agentName: "inner"}}}
	finalOut := EventFromMessage(schema.AssistantMessage("done", nil), nil, schema.Assistant, "")

	sub := &emitEventsAgent{events: []*AgentEvent{innerTransfer, finalOut}}
	fa := toFlowAgent(ctx, sub)

	it := fa.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("q")}})

	var outputs int
	for {
		ev, ok := it.Next()
		if !ok {
			break
		}
		if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming {
			if ev.Output.MessageOutput.Message != nil {
				outputs++
			}
		}
	}
	if outputs == 0 {
		t.Fatalf("no outputs observed; parent may have transferred on inner transfer action")
	}
}

type streamAgent struct{}

func (s *streamAgent) Name(context.Context) string        { return "s" }
func (s *streamAgent) Description(context.Context) string { return "s" }
func (s *streamAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	it, gen := NewAsyncIteratorPair[*AgentEvent]()
	go func() {
		frames := []*schema.Message{
			schema.AssistantMessage("hello ", nil),
			schema.AssistantMessage("world", nil),
		}
		stream := schema.StreamReaderFromArray(frames)
		gen.Send(EventFromMessage(nil, stream, schema.Assistant, ""))
		gen.Close()
	}()
	return it
}

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

	at := NewAgentTool(ctx, &streamAgent{})
	info, err := at.Info(ctx)
	assert.NoError(t, err)
	assert.Equal(t, "s", info.Name)
	assert.Equal(t, "s", info.Desc)
	js, err := info.ParamsOneOf.ToJSONSchema()
	assert.NoError(t, err)
	found := false
	for _, r := range js.Required {
		if r == "request" {
			found = true
			break
		}
	}
	assert.True(t, found)
	prop, ok := js.Properties.Get("request")
	assert.True(t, ok)
	assert.Equal(t, string(schema.String), prop.Type)

	custom := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
		"x": {Desc: "arg", Required: true, Type: schema.String},
	})
	at2 := NewAgentTool(ctx, &streamAgent{}, WithAgentInputSchema(custom))
	info2, err := at2.Info(ctx)
	assert.NoError(t, err)
	assert.Equal(t, custom, info2.ParamsOneOf)
	out, err := at.(tool.InvokableTool).InvokableRun(ctx, `{"request":"x"}`)
	assert.NoError(t, err)
	assert.Equal(t, "hello world", out)
}

type emptyAgent struct{}

func (e *emptyAgent) Name(context.Context) string        { return "empty" }
func (e *emptyAgent) Description(context.Context) string { return "empty" }
func (e *emptyAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	it, gen := NewAsyncIteratorPair[*AgentEvent]()
	go func() { gen.Close() }()
	return it
}

type noOutputAgent struct{}

func (n *noOutputAgent) Name(context.Context) string        { return "no" }
func (n *noOutputAgent) Description(context.Context) string { return "no" }
func (n *noOutputAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	it, gen := NewAsyncIteratorPair[*AgentEvent]()
	go func() { gen.Send(&AgentEvent{}); gen.Close() }()
	return it
}

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

	atEmpty := NewAgentTool(ctx, &emptyAgent{})
	out, err := atEmpty.(tool.InvokableTool).InvokableRun(ctx, `{"request":"x"}`)
	assert.Equal(t, "", out)
	assert.Error(t, err)

	atNo := NewAgentTool(ctx, &noOutputAgent{})
	out2, err := atNo.(tool.InvokableTool).InvokableRun(ctx, `{"request":"x"}`)
	assert.NoError(t, err)
	assert.Equal(t, "", out2)
}

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

	innerModel := &mockAgenticModel{
		generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) {
			return agenticMsg("inner result"), nil
		},
	}

	innerAgent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
		Name:        "AgenticInner",
		Description: "An agentic agent used as a tool",
		Model:       innerModel,
	})
	require.NoError(t, err)

	agenticAgentTool := NewTypedAgentTool(ctx, TypedAgent[*schema.AgenticMessage](innerAgent))

	var outerCallCount int32
	outerModel := &mockChatModelForAttack{
		generateFn: func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
			count := atomic.AddInt32(&outerCallCount, 1)
			if count == 1 {
				return &schema.Message{
					Role: schema.Assistant,
					ToolCalls: []schema.ToolCall{
						{ID: "c1", Function: schema.FunctionCall{Name: "AgenticInner", Arguments: `{"request":"test"}`}},
					},
				}, nil
			}
			return schema.AssistantMessage("done", nil), nil
		},
	}

	outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "OuterMessageAgent",
		Description: "A Message agent using an AgenticMessage sub-agent tool",
		Model:       outerModel,
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{agenticAgentTool},
			},
		},
	})
	require.NoError(t, err)

	runner := NewRunner(ctx, RunnerConfig{Agent: outerAgent, EnableStreaming: true})
	iter := runner.Query(ctx, "test cross-type")

	var capturedErr error
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		if event.Err != nil {
			capturedErr = event.Err
			t.Logf("Cross-type error message: %v", event.Err)
		}
	}

	if capturedErr == nil {
		t.Log("DESIGN CONCERN: Cross-type agent tool (AgenticMessage sub-agent in Message agent) " +
			"only errors at event forwarding time when streaming is enabled. " +
			"The error check happens in the gen.Send path, which is only exercised " +
			"when the outer agent actually calls the tool AND streaming is enabled. " +
			"Without streaming, the tool result is returned as a string, so no type mismatch occurs.")
	} else {
		assert.Contains(t, capturedErr.Error(), "cross-message-type",
			"Error should mention cross-message-type incompatibility")
	}
}
