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

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

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

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

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 integrationCheckpointStore struct {
	data map[string][]byte
}

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

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

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

type defaultPlan struct {
	Steps []string `json:"steps"`
}

func (p *defaultPlan) FirstStep() string {
	if len(p.Steps) == 0 {
		return ""
	}
	return p.Steps[0]
}

func (p *defaultPlan) MarshalJSON() ([]byte, error) {
	type planTyp defaultPlan
	return sonic.Marshal((*planTyp)(p))
}

func (p *defaultPlan) UnmarshalJSON(bytes []byte) error {
	type planTyp defaultPlan
	return sonic.Unmarshal(bytes, (*planTyp)(p))
}

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 formatRunPath(runPath []adk.RunStep) string {
	if len(runPath) == 0 {
		return "[]"
	}
	var parts []string
	for _, step := range runPath {
		parts = append(parts, step.String())
	}
	return "[" + strings.Join(parts, " -> ") + "]"
}

func formatAgentEventIntegration(event *adk.AgentEvent) string {
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("{AgentName: %q, RunPath: %s", event.AgentName, formatRunPath(event.RunPath)))
	if event.Output != nil {
		if event.Output.MessageOutput != nil && event.Output.MessageOutput.Message != nil {
			msg := event.Output.MessageOutput.Message
			sb.WriteString(fmt.Sprintf(", Output.Message: {Role: %q, Content: %q}", msg.Role, msg.Content))
		}
	}
	if event.Action != nil {
		if event.Action.Interrupted != nil {
			sb.WriteString(fmt.Sprintf(", Action.Interrupted: {%d contexts}", len(event.Action.Interrupted.InterruptContexts)))
		}
		if event.Action.BreakLoop != nil {
			sb.WriteString(fmt.Sprintf(", Action.BreakLoop: {From: %q, Done: %v}", event.Action.BreakLoop.From, event.Action.BreakLoop.Done))
		}
		if event.Action.TransferToAgent != nil {
			sb.WriteString(fmt.Sprintf(", Action.TransferToAgent: {Dest: %q}", event.Action.TransferToAgent.DestAgentName))
		}
	}
	if event.Err != nil {
		sb.WriteString(fmt.Sprintf(", Err: %v", event.Err))
	}
	sb.WriteString("}")
	return sb.String()
}

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

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

	mockSupervisorModel := mockModel.NewMockToolCallingChatModel(ctrl)
	mockPlannerModel := mockModel.NewMockToolCallingChatModel(ctrl)
	mockExecutorModel := mockModel.NewMockToolCallingChatModel(ctrl)
	mockReplannerModel := mockModel.NewMockToolCallingChatModel(ctrl)

	budgetTool := &approvableTool{name: "allocate_budget", t: t}

	plan := &defaultPlan{Steps: []string{"Allocate budget for the project", "Complete task"}}
	userInput := []adk.Message{schema.UserMessage("Set up a new project with budget allocation")}

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

	planJSON, _ := sonic.MarshalString(plan)
	plannerResponse := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "plan_call_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "plan",
				Arguments: planJSON,
			},
		},
	})
	plannerModelWithTools.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, input []*schema.Message, opts ...interface{}) (*schema.StreamReader[*schema.Message], error) {
			sr, sw := schema.Pipe[*schema.Message](1)
			go func() {
				defer sw.Close()
				sw.Send(plannerResponse, nil)
			}()
			return sr, nil
		},
	).Times(1)

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

	toolCallMsg := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "call_budget_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "allocate_budget",
				Arguments: `{"action": "allocate $50000 for project"}`,
			},
		},
	})
	mockExecutorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(toolCallMsg, nil).Times(1)

	completionMsg := schema.AssistantMessage("Budget allocated successfully", nil)
	mockExecutorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(completionMsg, nil).AnyTimes()

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

	respondResponse := schema.AssistantMessage("", []schema.ToolCall{
		{
			ID:   "respond_call_1",
			Type: "function",
			Function: schema.FunctionCall{
				Name:      "respond",
				Arguments: `{"response":"Project setup completed with budget allocation"}`,
			},
		},
	})
	replannerModelWithTools.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, input []*schema.Message, opts ...interface{}) (*schema.StreamReader[*schema.Message], error) {
			sr, sw := schema.Pipe[*schema.Message](1)
			go func() {
				defer sw.Close()
				sw.Send(respondResponse, nil)
			}()
			return sr, nil
		},
	).AnyTimes()

	plannerAgent, err := planexecute.NewPlanner(ctx, &planexecute.PlannerConfig{
		ToolCallingChatModel: mockPlannerModel,
	})
	assert.NoError(t, err)

	executorAgent, err := planexecute.NewExecutor(ctx, &planexecute.ExecutorConfig{
		Model: mockExecutorModel,
		ToolsConfig: adk.ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{budgetTool},
			},
		},
	})
	assert.NoError(t, err)

	replannerAgent, err := planexecute.NewReplanner(ctx, &planexecute.ReplannerConfig{
		ChatModel: mockReplannerModel,
	})
	assert.NoError(t, err)

	planExecuteAgent, err := planexecute.New(ctx, &planexecute.Config{
		Planner:       plannerAgent,
		Executor:      executorAgent,
		Replanner:     replannerAgent,
		MaxIterations: 10,
	})
	assert.NoError(t, err)

	projectAgent := &namedAgent{
		ResumableAgent: planExecuteAgent,
		name:           "project_execution_agent",
		description:    "the agent responsible for complex project execution tasks",
	}

	var pa adk.Agent = projectAgent

	_, ok := pa.(adk.ResumableAgent)
	assert.True(t, ok)

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

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

	finalSupervisorMsg := schema.AssistantMessage("Project setup completed successfully with budget allocation approved.", nil)
	mockSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(finalSupervisorMsg, nil).AnyTimes()

	supervisorChatAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:        "project_manager",
		Description: "the supervisor agent responsible for coordinating project management tasks",
		Instruction: "You are a project manager supervisor. Delegate complex project tasks to project_execution_agent.",
		Model:       mockSupervisorModel,
		Exit:        &adk.ExitTool{},
	})
	assert.NoError(t, err)

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

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

	t.Log("========================================")
	t.Log("Starting Supervisor + PlanExecute Integration Test")
	t.Log("========================================")

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

	var interruptEvent *adk.AgentEvent
	eventCount := 0
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		eventCount++
		t.Logf("Event %d: %s", eventCount, formatAgentEventIntegration(event))

		if event.Err != nil {
			t.Logf("Event has error: %v", event.Err)
		}

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

	if interruptEvent == nil {
		t.Fatal("Expected an interrupt event from the approvable 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")

	t.Logf("Interrupt event received with %d contexts", len(interruptEvent.Action.Interrupted.InterruptContexts))
	for i, ctx := range interruptEvent.Action.Interrupted.InterruptContexts {
		t.Logf("Interrupt context %d: ID=%s, Info=%v, IsRootCause=%v", i, ctx.ID, ctx.Info, ctx.IsRootCause)
	}

	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.Log("========================================")
	t.Logf("Resuming with approval for interrupt ID: %s", toolInterruptID)
	t.Log("========================================")

	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, hasBreakLoop 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.BreakLoop != nil && event.Action.BreakLoop.Done {
			hasBreakLoop = true
		}
	}

	assert.True(t, hasToolResponse, "Should have tool response indicating successful execution")
	assert.True(t, hasBreakLoop, "Should have break loop action indicating task completion")
}
