/*
 * 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 supervisor

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"testing"

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

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

// TestNewSupervisor tests the New function
func TestNewSupervisor(t *testing.T) {
	ctx := context.Background()

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

	// Create mock agents
	supervisorAgent := mockAdk.NewMockAgent(ctrl)
	subAgent1 := mockAdk.NewMockAgent(ctrl)
	subAgent2 := mockAdk.NewMockAgent(ctrl)

	supervisorAgent.EXPECT().Name(gomock.Any()).Return("SupervisorAgent").AnyTimes()
	supervisorAgent.EXPECT().Description(gomock.Any()).Return("Supervisor agent description").AnyTimes()
	subAgent1.EXPECT().Name(gomock.Any()).Return("SubAgent1").AnyTimes()
	subAgent2.EXPECT().Name(gomock.Any()).Return("SubAgent2").AnyTimes()

	aMsg, tMsg := adk.GenTransferMessages(ctx, "SubAgent1")
	i, g := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	g.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, ""))
	event := adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName)
	event.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "SubAgent1"}}
	g.Send(event)
	g.Close()
	supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1)

	i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	subAgent1Msg := schema.AssistantMessage("SubAgent1", nil)
	g.Send(adk.EventFromMessage(subAgent1Msg, nil, schema.Assistant, ""))
	g.Close()
	subAgent1.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1)

	aMsg, tMsg = adk.GenTransferMessages(ctx, "SubAgent2 message")
	i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	g.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, ""))
	event = adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName)
	event.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "SubAgent2"}}
	g.Send(event)
	g.Close()
	supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1)

	i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	subAgent2Msg := schema.AssistantMessage("SubAgent2 message", nil)
	g.Send(adk.EventFromMessage(subAgent2Msg, nil, schema.Assistant, ""))
	g.Close()
	subAgent2.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1)

	i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	finishMsg := schema.AssistantMessage("finish", nil)
	g.Send(adk.EventFromMessage(finishMsg, nil, schema.Assistant, ""))
	g.Close()
	supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1)

	conf := &Config{
		Supervisor: supervisorAgent,
		SubAgents:  []adk.Agent{subAgent1, subAgent2},
	}

	multiAgent, err := New(ctx, conf)
	assert.NoError(t, err)
	assert.NotNil(t, multiAgent)
	assert.Equal(t, "SupervisorAgent", multiAgent.Name(ctx))

	runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: multiAgent})
	aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("test")})

	// transfer to agent1
	event, ok := aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SupervisorAgent", event.AgentName)
	assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role)
	assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls))

	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SupervisorAgent", event.AgentName)
	assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role)
	assert.Equal(t, "SubAgent1", event.Action.TransferToAgent.DestAgentName)

	// agent1's output
	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SubAgent1", event.AgentName)
	assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role)
	assert.Equal(t, subAgent1Msg.Content, event.Output.MessageOutput.Message.Content)

	// transfer back to supervisor
	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SubAgent1", event.AgentName)
	assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role)
	assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls))

	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SubAgent1", event.AgentName)
	assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role)
	assert.Equal(t, "SupervisorAgent", event.Action.TransferToAgent.DestAgentName)

	// transfer to agent2
	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SupervisorAgent", event.AgentName)
	assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role)
	assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls))

	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SupervisorAgent", event.AgentName)
	assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role)
	assert.Equal(t, "SubAgent2", event.Action.TransferToAgent.DestAgentName)

	// agent1's output
	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SubAgent2", event.AgentName)
	assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role)
	assert.Equal(t, subAgent2Msg.Content, event.Output.MessageOutput.Message.Content)

	// transfer back to supervisor
	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SubAgent2", event.AgentName)
	assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role)
	assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls))

	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SubAgent2", event.AgentName)
	assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role)
	assert.Equal(t, "SupervisorAgent", event.Action.TransferToAgent.DestAgentName)

	// finish
	event, ok = aIter.Next()
	assert.True(t, ok)
	assert.Equal(t, "SupervisorAgent", event.AgentName)
	assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role)
	assert.Equal(t, finishMsg.Content, event.Output.MessageOutput.Message.Content)
}

type approvalInfo struct {
	ToolName        string
	ArgumentsInJSON string
	ToolCallID      string
}

func (ai *approvalInfo) String() string {
	return fmt.Sprintf("tool '%s' interrupted with arguments '%s', waiting for approval",
		ai.ToolName, ai.ArgumentsInJSON)
}

type approvalResult struct {
	Approved         bool
	DisapproveReason *string
}

func init() {
	schema.Register[*approvalInfo]()
	schema.Register[*approvalResult]()
}

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

func (m *approvableTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: m.name,
		Desc: "A tool that requires approval before execution",
		ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
			"action": {Type: schema.String, Desc: "The action to perform"},
		}),
	}, nil
}

func (m *approvableTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
	wasInterrupted, _, storedArguments := tool.GetInterruptState[string](ctx)
	if !wasInterrupted {
		return "", tool.StatefulInterrupt(ctx, &approvalInfo{
			ToolName:        m.name,
			ArgumentsInJSON: argumentsInJSON,
			ToolCallID:      compose.GetToolCallID(ctx),
		}, argumentsInJSON)
	}

	isResumeTarget, hasData, data := tool.GetResumeContext[*approvalResult](ctx)
	if !isResumeTarget {
		return "", tool.StatefulInterrupt(ctx, &approvalInfo{
			ToolName:        m.name,
			ArgumentsInJSON: storedArguments,
			ToolCallID:      compose.GetToolCallID(ctx),
		}, storedArguments)
	}

	if !hasData {
		return "", fmt.Errorf("tool '%s' resumed with no data", m.name)
	}

	if data.Approved {
		return fmt.Sprintf("Tool '%s' executed successfully with args: %s", m.name, storedArguments), nil
	}

	if data.DisapproveReason != nil {
		return fmt.Sprintf("Tool '%s' disapproved, reason: %s", m.name, *data.DisapproveReason), nil
	}

	return fmt.Sprintf("Tool '%s' disapproved", m.name), nil
}

type checkpointStore struct {
	data map[string][]byte
}

func newCheckpointStore() *checkpointStore {
	return &checkpointStore{data: make(map[string][]byte)}
}

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

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

type namedAgent struct {
	adk.ResumableAgent
	name        string
	description string
}

func (n *namedAgent) Name(_ context.Context) string {
	return n.name
}

func (n *namedAgent) Description(_ context.Context) string {
	return n.description
}

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockOuterSupervisorModel := mockModel.NewMockToolCallingChatModel(ctrl)
	mockInnerSupervisorModel := mockModel.NewMockToolCallingChatModel(ctrl)
	mockWorkerModel := mockModel.NewMockToolCallingChatModel(ctrl)

	paymentTool := &approvableTool{name: "process_payment", t: t}

	userInput := []adk.Message{schema.UserMessage("Process a payment of $1000")}

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

	workerToolCallMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "call_payment_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "process_payment",
				Arguments: `{"action": "process $1000 payment"}`,
			},
		},
	})
	mockWorkerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(workerToolCallMsg, nil).Times(1)

	workerCompletionMsg := schema.AssistantMessage("Payment processed successfully", nil)
	mockWorkerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(workerCompletionMsg, nil).AnyTimes()

	workerAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "payment_worker",
		Description: "the agent responsible for processing payments",
		Instruction: "You are a payment processing worker. Use the process_payment tool to handle payments.",
		Model:       mockWorkerModel,
		ToolsConfig: adk.ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{paymentTool},
			},
		},
	})
	assert.NoError(t, err)

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

	innerTransferMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "inner_transfer_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "transfer_to_agent",
				Arguments: `{"agent_name":"payment_worker"}`,
			},
		},
	})
	mockInnerSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(innerTransferMsg, nil).Times(1)

	innerFinalMsg := schema.AssistantMessage("Payment has been processed and approved.", nil)
	mockInnerSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(innerFinalMsg, nil).AnyTimes()

	innerSupervisorChatAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "payment_supervisor",
		Description: "the supervisor agent responsible for payment operations",
		Instruction: "You are a payment supervisor. Delegate payment tasks to payment_worker.",
		Model:       mockInnerSupervisorModel,
		Exit:        &adk.ExitTool{},
	})
	assert.NoError(t, err)

	innerSupervisorAgent, err := New(ctx, &Config{
		Supervisor: innerSupervisorChatAgent,
		SubAgents:  []adk.Agent{workerAgent},
	})
	assert.NoError(t, err)

	innerSupervisorWrapped := &namedAgent{
		ResumableAgent: innerSupervisorAgent,
		name:           "payment_department",
		description:    "the department responsible for all payment-related operations",
	}

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

	outerTransferMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "outer_transfer_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "transfer_to_agent",
				Arguments: `{"agent_name":"payment_department"}`,
			},
		},
	})
	mockOuterSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(outerTransferMsg, nil).Times(1)

	outerFinalMsg := schema.AssistantMessage("The payment request has been fully processed by the payment department.", nil)
	mockOuterSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(outerFinalMsg, nil).AnyTimes()

	outerSupervisorChatAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "company_coordinator",
		Description: "the top-level coordinator for company operations",
		Instruction: "You are the company coordinator. Route payment requests to payment_department.",
		Model:       mockOuterSupervisorModel,
		Exit:        &adk.ExitTool{},
	})
	assert.NoError(t, err)

	outerSupervisorAgent, err := New(ctx, &Config{
		Supervisor: outerSupervisorChatAgent,
		SubAgents:  []adk.Agent{innerSupervisorWrapped},
	})
	assert.NoError(t, err)

	outerSupervisorWrapped := &namedAgent{
		ResumableAgent: outerSupervisorAgent,
		name:           "headquarters",
		description:    "the company headquarters that coordinates all departments",
	}

	store := newCheckpointStore()
	runner := adk.NewRunner(ctx, adk.RunnerConfig{
		Agent:           outerSupervisorWrapped,
		CheckPointStore: store,
	})

	t.Log("========================================")
	t.Log("Starting Nested Supervisor Integration Test (with namedAgent wrappers)")
	t.Log("Hierarchy: headquarters(wrapper) -> company_coordinator -> payment_department(wrapper) -> payment_supervisor -> payment_worker -> process_payment tool")
	t.Log("========================================")

	checkpointID := "test-nested-supervisor-1"
	iter := runner.Run(ctx, userInput, adk.WithCheckPointID(checkpointID))

	var interruptEvent *adk.AgentEvent
	eventCount := 0
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		eventCount++

		if event.Action != nil && event.Action.Interrupted != nil {
			interruptEvent = event
			t.Log("INTERRUPT DETECTED - Deep interrupt from tool within nested supervisor")
			break
		}
	}

	if interruptEvent == nil {
		t.Fatal("Expected an interrupt event from the process_payment tool, but none was received")
	}

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

	var toolInterruptID string
	for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts {
		if intCtx.IsRootCause {
			toolInterruptID = intCtx.ID
			break
		}
	}
	assert.NotEmpty(t, toolInterruptID, "Should have a root cause interrupt ID")

	t.Logf("Resuming with approval for interrupt ID: %s", toolInterruptID)

	resumeIter, err := runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{
		Targets: map[string]any{
			toolInterruptID: &approvalResult{Approved: true},
		},
	})
	assert.NoError(t, err, "Resume should not error")
	assert.NotNil(t, resumeIter, "Resume iterator should not be nil")

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

	assert.NotEmpty(t, resumeEvents, "Should have resume events after approval")

	for _, event := range resumeEvents {
		assert.NoError(t, event.Err, "Resume event should not have error")
	}

	var hasToolResponse, hasTransferBack bool
	for _, event := range resumeEvents {
		if event.Output != nil && event.Output.MessageOutput != nil {
			msg := event.Output.MessageOutput.Message
			if msg != nil && msg.Role == "tool" && strings.Contains(msg.Content, "executed successfully") {
				hasToolResponse = true
			}
		}
		if event.Action != nil && event.Action.TransferToAgent != nil {
			if event.Action.TransferToAgent.DestAgentName == "company_coordinator" {
				hasTransferBack = true
			}
		}
	}

	assert.True(t, hasToolResponse, "Should have tool response indicating successful payment processing")
	assert.True(t, hasTransferBack, "Should have transfer back to outer supervisor indicating completion")
}

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

	supervisorAgent := mockAdk.NewMockAgent(ctrl)
	subAgent := mockAdk.NewMockAgent(ctrl)

	supervisorAgent.EXPECT().Name(gomock.Any()).Return("Supervisor").AnyTimes()
	supervisorAgent.EXPECT().Description(gomock.Any()).Return("Supervisor description").AnyTimes()
	subAgent.EXPECT().Name(gomock.Any()).Return("SubAgent").AnyTimes()

	// 1. Supervisor transfers to SubAgent
	aMsg, tMsg := adk.GenTransferMessages(ctx, "SubAgent")
	i1, g1 := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	g1.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, ""))
	event1 := adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName)
	event1.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "SubAgent"}}
	g1.Send(event1)
	g1.Close()
	supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i1).Times(1)

	// 2. SubAgent emits Exit action
	i2, g2 := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	exitEvent := &adk.AgentEvent{
		AgentName: "SubAgent",
		Action:    &adk.AgentAction{Exit: true},
		Output: &adk.AgentOutput{
			MessageOutput: &adk.MessageVariant{
				Role:    schema.Assistant,
				Message: schema.AssistantMessage("Exiting...", nil),
			},
		},
	}
	g2.Send(exitEvent)
	g2.Close()
	subAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i2).Times(1)

	conf := &Config{
		Supervisor: supervisorAgent,
		SubAgents:  []adk.Agent{subAgent},
	}

	multiAgent, err := New(ctx, conf)
	assert.NoError(t, err)

	runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: multiAgent})
	aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("test")})

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

	foundExit := false
	foundTransferBack := false

	for _, e := range events {
		if e.Action != nil {
			if e.Action.Exit {
				foundExit = true
			}
			if e.Action.TransferToAgent != nil && e.Action.TransferToAgent.DestAgentName == "Supervisor" {
				foundTransferBack = true
			}
		}
	}

	assert.True(t, foundExit, "Should have found Exit action")
	assert.False(t, foundTransferBack, "Should NOT have found Transfer back to Supervisor after Exit")
}

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

	topSupervisor := mockAdk.NewMockAgent(ctrl)
	midSupervisor := mockAdk.NewMockAgent(ctrl)
	worker := mockAdk.NewMockAgent(ctrl)

	topSupervisor.EXPECT().Name(gomock.Any()).Return("TopSupervisor").AnyTimes()
	topSupervisor.EXPECT().Description(gomock.Any()).Return("Top supervisor description").AnyTimes()
	midSupervisor.EXPECT().Name(gomock.Any()).Return("MidSupervisor").AnyTimes()
	midSupervisor.EXPECT().Description(gomock.Any()).Return("Mid supervisor description").AnyTimes()
	worker.EXPECT().Name(gomock.Any()).Return("Worker").AnyTimes()

	// 1. TopSupervisor transfers to MidSupervisor
	aMsg1, tMsg1 := adk.GenTransferMessages(ctx, "MidSupervisor")
	i1, g1 := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	g1.Send(adk.EventFromMessage(aMsg1, nil, schema.Assistant, ""))
	event1 := adk.EventFromMessage(tMsg1, nil, schema.Tool, tMsg1.ToolName)
	event1.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "MidSupervisor"}}
	g1.Send(event1)
	g1.Close()
	topSupervisor.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i1).AnyTimes()

	// 2. MidSupervisor transfers to Worker
	aMsg2, tMsg2 := adk.GenTransferMessages(ctx, "Worker")
	i2, g2 := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	g2.Send(adk.EventFromMessage(aMsg2, nil, schema.Assistant, ""))
	event2 := adk.EventFromMessage(tMsg2, nil, schema.Tool, tMsg2.ToolName)
	event2.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "Worker"}}
	g2.Send(event2)
	g2.Close()
	midSupervisor.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i2).AnyTimes()

	// 3. Worker emits Exit action
	i3, g3 := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
	exitEvent := &adk.AgentEvent{
		AgentName: "Worker",
		Action:    &adk.AgentAction{Exit: true},
		Output: &adk.AgentOutput{
			MessageOutput: &adk.MessageVariant{
				Role:    schema.Assistant,
				Message: schema.AssistantMessage("Worker Exiting...", nil),
			},
		},
	}
	g3.Send(exitEvent)
	g3.Close()
	worker.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i3).Times(1)

	// Build Nested System
	// Mid System: MidSupervisor -> [Worker]
	midSystem, err := New(ctx, &Config{
		Supervisor: midSupervisor,
		SubAgents:  []adk.Agent{worker},
	})
	assert.NoError(t, err)
	// We need to give the midSystem the name "MidSupervisor" so TopSupervisor can find it
	// supervisor.New returns a ResumableAgent that delegates Name() to the supervisor agent.
	// So midSystem.Name() should already be "MidSupervisor" because midSupervisor.Name() is "MidSupervisor".

	// Top System: TopSupervisor -> [midSystem]
	topSystem, err := New(ctx, &Config{
		Supervisor: topSupervisor,
		SubAgents:  []adk.Agent{midSystem},
	})
	assert.NoError(t, err)

	runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: topSystem})
	aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("test nested exit")})

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

	foundExit := false
	foundTransferBackToMidAfterExit := false
	foundTransferBackToTopAfterExit := false

	for _, e := range events {
		if e.Action != nil {
			if e.Action.Exit {
				foundExit = true
			}
			if foundExit && e.Action.TransferToAgent != nil {
				if e.Action.TransferToAgent.DestAgentName == "MidSupervisor" {
					foundTransferBackToMidAfterExit = true
				}
				if e.Action.TransferToAgent.DestAgentName == "TopSupervisor" {
					foundTransferBackToTopAfterExit = true
				}
			}
		}
	}

	assert.True(t, foundExit, "Should have found Exit action")
	assert.False(t, foundTransferBackToMidAfterExit, "Should NOT have found Transfer back to MidSupervisor after Exit")
	assert.False(t, foundTransferBackToTopAfterExit, "Should NOT have found Transfer back to TopSupervisor after Exit")
}

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

	supervisorAgent := mockAdk.NewMockAgent(ctrl)
	workerModel := mockModel.NewMockToolCallingChatModel(ctrl)
	innerAgent := mockAdk.NewMockAgent(ctrl)

	supervisorAgent.EXPECT().Name(gomock.Any()).Return("Supervisor").AnyTimes()
	supervisorAgent.EXPECT().Description(gomock.Any()).Return("Supervisor description").AnyTimes()
	innerAgent.EXPECT().Name(gomock.Any()).Return("InnerAgent").AnyTimes()
	innerAgent.EXPECT().Description(gomock.Any()).Return("Inner Agent Description").AnyTimes()

	// 1. Supervisor transfers to Worker (only once, then exits when worker transfers back)
	supervisorRunCount := 0
	supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
			supervisorRunCount++
			iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
			go func() {
				defer gen.Close()
				if supervisorRunCount == 1 {
					aMsg, tMsg := adk.GenTransferMessages(ctx, "Worker")
					gen.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, ""))
					event1 := adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName)
					event1.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "Worker"}}
					gen.Send(event1)
				} else {
					exitEvent := &adk.AgentEvent{
						AgentName: "Supervisor",
						Action:    &adk.AgentAction{Exit: true},
						Output: &adk.AgentOutput{
							MessageOutput: &adk.MessageVariant{
								Role:    schema.Assistant,
								Message: schema.AssistantMessage("Supervisor done", nil),
							},
						},
					}
					gen.Send(exitEvent)
				}
			}()
			return iter
		}).AnyTimes()

	// 2. Worker runs, calls AgentTool (InnerAgent)
	// Mock WorkerModel behavior
	workerModel.EXPECT().WithTools(gomock.Any()).Return(workerModel, nil).AnyTimes()

	// 2.1 Worker generates tool call
	toolCallMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "call_inner_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "InnerAgent",
				Arguments: `{"request": "do exit"}`,
			},
		},
	})
	workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(toolCallMsg, nil).Times(1)

	// 2.2 InnerAgent runs and emits Exit
	innerAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
			iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
			go func() {
				defer gen.Close()
				innerExitEvent := &adk.AgentEvent{
					AgentName: "InnerAgent",
					Action:    &adk.AgentAction{Exit: true},
					RunPath:   []adk.RunStep{},
					Output: &adk.AgentOutput{
						MessageOutput: &adk.MessageVariant{
							Role:    schema.Assistant,
							Message: schema.AssistantMessage("Inner Exiting...", nil),
						},
					},
				}
				gen.Send(innerExitEvent)
			}()
			return iter
		}).AnyTimes()

	// 2.3 Worker receives tool result (empty string or whatever AgentTool returns on exit/interrupt)
	// AgentTool implementation details: if Exit action is present, it returns whatever output is there.
	// The Exit action itself is passed as internal event.

	// 2.4 Worker generates final response
	finalMsg := schema.AssistantMessage("Worker Finished", nil)
	workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(finalMsg, nil).AnyTimes()

	// Build Worker Agent
	agentTool := adk.NewAgentTool(ctx, innerAgent)
	workerAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "Worker",
		Description: "Worker Agent",
		Model:       workerModel,
		ToolsConfig: adk.ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{agentTool},
			},
			EmitInternalEvents: true, // Key configuration
		},
	})
	assert.NoError(t, err)

	// Build System
	sys, err := New(ctx, &Config{
		Supervisor: supervisorAgent,
		SubAgents:  []adk.Agent{workerAgent},
	})
	assert.NoError(t, err)

	runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: sys})
	aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("start")})

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

	foundInnerExit := false
	foundTransferBack := false

	for _, e := range events {
		// Check for InnerAgent exit event (propagated as internal event)
		if e.AgentName == "InnerAgent" && e.Action != nil && e.Action.Exit {
			foundInnerExit = true
		}

		// Check for transfer back to Supervisor
		if e.AgentName == "Worker" && e.Action != nil && e.Action.TransferToAgent != nil &&
			e.Action.TransferToAgent.DestAgentName == "Supervisor" {
			foundTransferBack = true
		}
	}

	assert.True(t, foundInnerExit, "Should have captured InnerAgent Exit event")
	assert.True(t, foundTransferBack, "Should have found Transfer back to Supervisor (Worker should NOT be considered exited)")
}

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

	supervisorModel := mockModel.NewMockToolCallingChatModel(ctrl)
	subAgentModel := mockModel.NewMockToolCallingChatModel(ctrl)

	supervisorModel.EXPECT().WithTools(gomock.Any()).Return(supervisorModel, nil).AnyTimes()
	subAgentModel.EXPECT().WithTools(gomock.Any()).Return(subAgentModel, nil).AnyTimes()

	transferMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "transfer_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "transfer_to_agent",
				Arguments: `{"agent_name":"SubAgent"}`,
			},
		},
	})
	supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(transferMsg, nil).Times(1)

	subAgentResponse := schema.AssistantMessage("SubAgent response", nil)
	subAgentModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(subAgentResponse, nil).Times(1)

	finalResponse := schema.AssistantMessage("Final response", nil)
	supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(finalResponse, nil).Times(1)

	supervisorAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "SupervisorAgent",
		Description: "Supervisor agent",
		Instruction: "You are a supervisor",
		Model:       supervisorModel,
	})
	assert.NoError(t, err)

	subAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "SubAgent",
		Description: "Sub agent",
		Instruction: "You are a sub agent",
		Model:       subAgentModel,
	})
	assert.NoError(t, err)

	multiAgent, err := New(ctx, &Config{
		Supervisor: supervisorAgent,
		SubAgents:  []adk.Agent{subAgent},
	})
	assert.NoError(t, err)

	assert.Equal(t, "SupervisorAgent", multiAgent.Name(ctx))

	typer, ok := multiAgent.(components.Typer)
	assert.True(t, ok, "Should implement components.Typer")
	assert.Equal(t, "Supervisor", typer.GetType())

	var mu sync.Mutex
	var onStartCalls []string
	var onEndCalls []string

	handler := callbacks.NewHandlerBuilder().
		OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
			if info.Component != adk.ComponentOfAgent {
				return ctx
			}
			mu.Lock()
			onStartCalls = append(onStartCalls, info.Name+":"+info.Type)
			mu.Unlock()
			return ctx
		}).
		OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
			if info.Component != adk.ComponentOfAgent {
				return ctx
			}
			mu.Lock()
			onEndCalls = append(onEndCalls, info.Name+":"+info.Type)
			mu.Unlock()
			if agentOutput := adk.ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil {
				go func() {
					for {
						_, ok := agentOutput.Events.Next()
						if !ok {
							break
						}
					}
				}()
			}
			return ctx
		}).
		Build()

	runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: multiAgent})
	iter := runner.Query(ctx, "hello", adk.WithCallbacks(handler))

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

	mu.Lock()
	defer mu.Unlock()

	assert.NotEmpty(t, onStartCalls, "Should have OnStart calls")
	assert.Contains(t, onStartCalls, "SupervisorAgent:Supervisor", "Should have supervisor container OnStart with type 'Supervisor'")
}

type traceContextKey struct{}

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

	supervisorModel := mockModel.NewMockToolCallingChatModel(ctrl)
	workerModel := mockModel.NewMockToolCallingChatModel(ctrl)

	supervisorModel.EXPECT().WithTools(gomock.Any()).Return(supervisorModel, nil).AnyTimes()
	workerModel.EXPECT().WithTools(gomock.Any()).Return(workerModel, nil).AnyTimes()

	paymentTool := &approvableTool{name: "process_payment", t: t}

	workerToolCallMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "call_payment_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "process_payment",
				Arguments: `{"action": "process $1000 payment"}`,
			},
		},
	})
	workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(workerToolCallMsg, nil).Times(1)

	workerCompletionMsg := schema.AssistantMessage("Payment processed successfully", nil)
	workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(workerCompletionMsg, nil).AnyTimes()

	workerAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "Worker",
		Description: "Worker agent",
		Instruction: "You are a worker",
		Model:       workerModel,
		ToolsConfig: adk.ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{paymentTool},
			},
		},
	})
	assert.NoError(t, err)

	transferMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "transfer_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "transfer_to_agent",
				Arguments: `{"agent_name":"Worker"}`,
			},
		},
	})
	supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(transferMsg, nil).Times(1)

	finalResponse := schema.AssistantMessage("Final response", nil)
	supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(finalResponse, nil).AnyTimes()

	supervisorAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "SupervisorAgent",
		Description: "Supervisor agent",
		Instruction: "You are a supervisor",
		Model:       supervisorModel,
		Exit:        &adk.ExitTool{},
	})
	assert.NoError(t, err)

	multiAgent, err := New(ctx, &Config{
		Supervisor: supervisorAgent,
		SubAgents:  []adk.Agent{workerAgent},
	})
	assert.NoError(t, err)

	store := newCheckpointStore()
	runner := adk.NewRunner(ctx, adk.RunnerConfig{
		Agent:           multiAgent,
		CheckPointStore: store,
	})

	var mu sync.Mutex
	var runOnStartCalls []string
	var resumeOnStartCalls []string
	var resumeParentTraceIDs []string

	runHandler := callbacks.NewHandlerBuilder().
		OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
			if info.Component != adk.ComponentOfAgent {
				return ctx
			}
			mu.Lock()
			runOnStartCalls = append(runOnStartCalls, info.Name+":"+info.Type)
			mu.Unlock()
			return ctx
		}).
		OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
			if info.Component != adk.ComponentOfAgent {
				return ctx
			}
			if agentOutput := adk.ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil {
				go func() {
					for {
						_, ok := agentOutput.Events.Next()
						if !ok {
							break
						}
					}
				}()
			}
			return ctx
		}).
		Build()

	checkpointID := "test-unified-tracing-resume"
	iter := runner.Run(ctx, []adk.Message{schema.UserMessage("Process payment")}, adk.WithCallbacks(runHandler), adk.WithCheckPointID(checkpointID))

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

	assert.NotNil(t, interruptEvent, "Should have interrupt event")

	var toolInterruptID string
	for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts {
		if intCtx.IsRootCause {
			toolInterruptID = intCtx.ID
			break
		}
	}
	assert.NotEmpty(t, toolInterruptID, "Should have a root cause interrupt ID")

	mu.Lock()
	t.Logf("Run OnStart calls: %v", runOnStartCalls)
	assert.Contains(t, runOnStartCalls, "SupervisorAgent:Supervisor", "Run should have supervisor container OnStart")
	mu.Unlock()

	resumeHandler := callbacks.NewHandlerBuilder().
		OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
			if info.Component != adk.ComponentOfAgent {
				return ctx
			}
			mu.Lock()
			resumeOnStartCalls = append(resumeOnStartCalls, info.Name+":"+info.Type)
			parentID, _ := ctx.Value(traceContextKey{}).(string)
			resumeParentTraceIDs = append(resumeParentTraceIDs, info.Name+":parent="+parentID)
			mu.Unlock()
			if info.Type == "Supervisor" {
				return context.WithValue(ctx, traceContextKey{}, "supervisor-trace-id")
			}
			return ctx
		}).
		OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
			if info.Component != adk.ComponentOfAgent {
				return ctx
			}
			if agentOutput := adk.ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil {
				go func() {
					for {
						_, ok := agentOutput.Events.Next()
						if !ok {
							break
						}
					}
				}()
			}
			return ctx
		}).
		Build()

	resumeIter, err := runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{
		Targets: map[string]any{
			toolInterruptID: &approvalResult{Approved: true},
		},
	}, adk.WithCallbacks(resumeHandler))
	assert.NoError(t, err)

	for {
		event, ok := resumeIter.Next()
		if !ok {
			break
		}
		assert.NoError(t, event.Err)
	}

	mu.Lock()
	defer mu.Unlock()

	t.Logf("Resume OnStart calls: %v", resumeOnStartCalls)
	t.Logf("Resume parent trace IDs: %v", resumeParentTraceIDs)
	assert.NotEmpty(t, resumeOnStartCalls, "Should have OnStart calls during resume")
	assert.Contains(t, resumeOnStartCalls, "SupervisorAgent:Supervisor", "Resume should have supervisor container OnStart with type 'Supervisor'")

	foundInnerSupervisorWithParent := false
	for _, entry := range resumeParentTraceIDs {
		if strings.Contains(entry, "SupervisorAgent") && !strings.Contains(entry, "parent=supervisor-trace-id") && entry != "SupervisorAgent:parent=" {
			if strings.Contains(resumeOnStartCalls[0], "Supervisor") {
				continue
			}
		}
		if strings.Contains(entry, "parent=supervisor-trace-id") {
			foundInnerSupervisorWithParent = true
		}
	}
	assert.True(t, foundInnerSupervisorWithParent,
		"Inner agents should have parent trace from Supervisor container during Resume. Got: %v", resumeParentTraceIDs)
}
