package test

import (
	stdContext "context"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
	"sync"
	"time"

	jsoniter "github.com/json-iterator/go"
	"github.com/yaoapp/yao/agent/assistant"
	"github.com/yaoapp/yao/agent/context"
)

// Executor executes test cases against an agent
type Executor struct {
	opts         *Options
	output       *OutputWriter
	resolver     Resolver
	loader       Loader
	hookExecutor *HookExecutor
	agentPath    string // Path to the agent being tested
}

// NewRunner creates a new test runner
func NewRunner(opts *Options) *Executor {
	return &Executor{
		opts:         opts,
		output:       NewOutputWriter(opts.Verbose),
		resolver:     NewResolver(),
		loader:       NewLoader(),
		hookExecutor: NewHookExecutor(opts.Verbose),
	}
}

// Run executes all test cases and returns a report
func (r *Executor) Run() (*Report, error) {
	// For script test mode, use script runner
	if r.opts.InputMode == InputModeScript {
		return r.RunScriptTests()
	}

	// For direct message mode, use simplified output (development mode)
	if r.opts.InputMode == InputModeMessage {
		return r.RunDirect()
	}

	return r.RunTests()
}

// RunScriptTests executes script tests and returns a report
func (r *Executor) RunScriptTests() (*Report, error) {
	scriptRunner := NewScriptRunner(r.opts)
	scriptReport, err := scriptRunner.Run()
	if err != nil {
		return nil, err
	}

	// Convert to standard report for unified output handling
	report := scriptReport.ToReport()

	if r.opts.JSONOutput {
		r.output.ScriptOutputJSON(scriptReport)
	}

	// Write output if specified
	if r.opts.OutputFile != "" {
		err = r.writeOutput(report)
		if err != nil {
			r.output.Error("Failed to write output: %s", err.Error())
		} else {
			r.output.OutputFile(r.opts.OutputFile)
		}
	}

	if !r.opts.JSONOutput {
		r.output.FinalResult(!report.HasFailures())
	}

	return report, nil
}

// RunDirect executes a single direct message and outputs the result directly
// This is optimized for development/debugging scenarios
func (r *Executor) RunDirect() (*Report, error) {
	// Resolve agent
	agentInfo, err := r.resolver.Resolve(r.opts)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve agent: %w", err)
	}

	// Get assistant
	ast, err := assistant.Get(agentInfo.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to get assistant: %w", err)
	}

	// Create test case from message
	tc := CreateTestCaseFromMessage(r.opts.Input)

	// Create context
	chatID := GenerateChatID(tc.ID, 1)
	ctx := NewTestContextFromOptions(chatID, agentInfo.ID, r.opts, tc)
	defer ctx.Release()

	// Build context options
	opts := buildContextOptions(tc, r.opts)

	// Create timeout context
	timeout := tc.GetTimeout(r.opts.Timeout)
	timeoutCtx, cancel := stdContext.WithTimeout(ctx.Context, timeout)
	defer cancel()
	ctx.Context = timeoutCtx

	// Parse input to messages with file loading support
	inputOpts := r.getInputOptions()
	messages, err := tc.GetMessagesWithOptions(inputOpts)
	if err != nil {
		return nil, fmt.Errorf("failed to parse input: %w", err)
	}

	// Run the agent
	start := time.Now()
	response, err := ast.Stream(ctx, messages, opts)

	// Check for timeout
	if timeoutCtx.Err() != nil {
		return nil, fmt.Errorf("timeout after %s", timeout)
	}

	// Check for error
	if err != nil {
		return nil, err
	}

	// Extract output and build trace
	output := extractOutput(response)
	trace := buildTrace(response)
	duration := time.Since(start)

	if r.opts.JSONOutput {
		r.output.DirectOutputJSON(output, trace, duration)
	} else {
		r.output.DirectOutput(output)
		r.output.DirectTrace(trace)
	}

	// Determine connector: user-specified > agent default
	connector := r.opts.Connector
	if connector == "" {
		connector = agentInfo.Connector
	}

	// Return minimal report (for exit code handling)
	return &Report{
		Summary: &Summary{
			Total:     1,
			Passed:    1,
			AgentID:   agentInfo.ID,
			Connector: connector,
		},
	}, nil
}

// RunTests executes test cases from file and generates a report
func (r *Executor) RunTests() (*Report, error) {
	startTime := time.Now()

	// Print header
	r.output.Header("Agent Test")

	// Resolve agent
	agentInfo, err := r.resolver.Resolve(r.opts)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve agent: %w", err)
	}

	r.output.Info("Agent: %s", agentInfo.ID)
	r.agentPath = agentInfo.Path // Store agent path for hook execution
	if r.opts.Connector != "" {
		r.output.Info("Connector: %s (override)", r.opts.Connector)
	} else if agentInfo.Connector != "" {
		r.output.Info("Connector: %s", agentInfo.Connector)
	}

	// Load test cases based on input source
	var testCases []*Case
	inputSource := ParseInputSource(r.opts.Input)

	switch inputSource.Type {
	case InputSourceAgent:
		// Generate test cases using agent
		r.output.Info("Generating test cases from agent: %s", inputSource.Value)
		targetInfo := &TargetAgentInfo{
			ID:          agentInfo.ID,
			Description: agentInfo.Description,
		}
		testCases, err = r.loader.LoadFromAgent(inputSource.Value, targetInfo, inputSource.Params)
		if err != nil {
			return nil, fmt.Errorf("failed to generate test cases: %w", err)
		}
		r.output.Info("Generated: %d test cases", len(testCases))

	case InputSourceScript:
		// Generate test cases using script (if it's a generator script, not test script)
		// Note: scripts. prefix without "scripts:" is handled by RunScriptTests
		if strings.HasPrefix(r.opts.Input, "scripts:") {
			scriptRef := strings.TrimPrefix(r.opts.Input, "scripts:")
			r.output.Info("Generating test cases from script: %s", scriptRef)
			targetInfo := &TargetAgentInfo{
				ID:          agentInfo.ID,
				Description: agentInfo.Description,
			}
			testCases, err = r.loader.LoadFromScript(scriptRef, targetInfo)
			if err != nil {
				return nil, fmt.Errorf("failed to generate test cases from script: %w", err)
			}
			r.output.Info("Generated: %d test cases", len(testCases))
		} else {
			// This is a test script (scripts.xxx format), handled by RunScriptTests
			return nil, fmt.Errorf("script test mode should be handled by RunScriptTests")
		}

	default:
		// File mode - load from JSONL
		testCases, err = r.loader.LoadFile(r.opts.Input)
		if err != nil {
			return nil, fmt.Errorf("failed to load test cases: %w", err)
		}
		r.output.Info("Input: %s (%d test cases)", r.opts.Input, len(testCases))
	}

	// Handle dry-run mode - just output the generated test cases
	if r.opts.DryRun {
		r.output.Info("Dry-run mode: outputting generated test cases")
		return r.outputDryRun(testCases, agentInfo)
	}

	// Filter skipped tests
	activeTests := FilterSkipped(testCases)
	skippedCount := len(testCases) - len(activeTests)
	if skippedCount > 0 {
		r.output.Warning("Skipped: %d test cases", skippedCount)
	}

	// Filter by --run pattern if specified
	if r.opts.Run != "" {
		runPattern, err := regexp.Compile(r.opts.Run)
		if err != nil {
			return nil, fmt.Errorf("invalid --run pattern %q: %w", r.opts.Run, err)
		}
		activeTests = FilterByPattern(activeTests, runPattern)
		if len(activeTests) == 0 {
			return nil, fmt.Errorf("no test cases match pattern %q", r.opts.Run)
		}
		r.output.Info("Filter: %q (%d test cases match)", r.opts.Run, len(activeTests))
	}

	// Load context config if specified
	if r.opts.ContextFile != "" {
		ctxConfig, err := LoadContextConfig(r.opts.ContextFile)
		if err != nil {
			return nil, fmt.Errorf("failed to load context file: %w", err)
		}
		r.opts.ContextData = ctxConfig
		r.output.Info("Context: %s", r.opts.ContextFile)
	}

	// Set options on hook executor (for context data access in hooks)
	r.hookExecutor.SetOptions(r.opts)

	// Print test info
	if r.opts.Runs > 1 {
		r.output.Info("Runs: %d per test case (stability analysis)", r.opts.Runs)
	}
	r.output.Info("Timeout: %s", r.opts.Timeout)
	if r.opts.Parallel > 1 {
		r.output.Info("Parallel: %d", r.opts.Parallel)
	}

	// Get assistant
	ast, err := assistant.Get(agentInfo.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to get assistant: %w", err)
	}

	// Determine connector: user-specified > agent default
	connector := r.opts.Connector
	if connector == "" {
		connector = agentInfo.Connector
	}

	// Create report
	report := &Report{
		Summary: &Summary{
			Total:       len(testCases),
			AgentID:     agentInfo.ID,
			AgentPath:   agentInfo.Path,
			Connector:   connector,
			RunsPerCase: r.opts.Runs,
		},
		Environment: NewEnvironment(r.opts.UserID, r.opts.TeamID),
		Metadata: &ReportMetadata{
			StartedAt: startTime,
			InputFile: r.opts.Input,
			Options:   r.opts,
		},
	}

	// Execute global BeforeAll if specified
	var globalBeforeData interface{}
	if r.opts.BeforeAll != "" {
		r.output.Info("BeforeAll: %s", r.opts.BeforeAll)
		var err error
		globalBeforeData, err = r.hookExecutor.ExecuteBeforeAll(r.opts.BeforeAll, activeTests, agentInfo.Path)
		if err != nil {
			return nil, fmt.Errorf("beforeAll script failed: %w", err)
		}
	}

	// Ensure AfterAll runs even if tests fail
	defer func() {
		if r.opts.AfterAll != "" {
			r.output.Info("AfterAll: %s", r.opts.AfterAll)
			if err := r.hookExecutor.ExecuteAfterAll(r.opts.AfterAll, report.Results, globalBeforeData, agentInfo.Path); err != nil {
				r.output.Warning("afterAll script failed: %s", err.Error())
			}
		}
	}()

	// Run tests
	r.output.SubHeader("Running Tests")

	if r.opts.Runs > 1 {
		// Stability testing mode
		report.StabilityResults = r.runStabilityTests(ast, activeTests, agentInfo.ID)
		r.calculateStabilitySummary(report)
	} else {
		// Single run mode
		report.Results = r.runSingleTests(ast, activeTests, agentInfo.ID)
		r.calculateSingleSummary(report)
	}

	// Add skipped count
	report.Summary.Skipped = skippedCount

	// Complete report
	report.Summary.DurationMs = time.Since(startTime).Milliseconds()
	report.Metadata.CompletedAt = time.Now()

	// Print summary
	r.output.Summary(report.Summary, time.Since(startTime))

	// Write output
	if r.opts.OutputFile != "" {
		err = r.writeOutput(report)
		if err != nil {
			r.output.Error("Failed to write output: %s", err.Error())
		} else {
			r.output.OutputFile(r.opts.OutputFile)
		}
	}

	// Print final result
	r.output.FinalResult(!report.HasFailures())

	return report, nil
}

// runSingleTests runs each test case once
func (r *Executor) runSingleTests(ast *assistant.Assistant, testCases []*Case, agentID string) []*Result {
	results := make([]*Result, 0, len(testCases))

	if r.opts.Parallel > 1 {
		// Parallel execution
		results = r.runParallel(ast, testCases, agentID)
	} else {
		// Sequential execution
		for i, tc := range testCases {
			result := r.runSingleTest(ast, tc, agentID, 1)
			results = append(results, result)

			// Check fail-fast
			if r.opts.FailFast && result.Status != StatusPassed && result.Status != StatusSkipped {
				r.output.Warning("Stopping due to --fail-fast (failed at test %d/%d)", i+1, len(testCases))
				break
			}
		}
	}

	return results
}

// runParallel runs tests in parallel
func (r *Executor) runParallel(ast *assistant.Assistant, testCases []*Case, agentID string) []*Result {
	results := make([]*Result, len(testCases))
	var wg sync.WaitGroup
	sem := make(chan struct{}, r.opts.Parallel)

	for i, tc := range testCases {
		wg.Add(1)
		go func(idx int, testCase *Case) {
			defer wg.Done()
			sem <- struct{}{}        // Acquire
			defer func() { <-sem }() // Release

			results[idx] = r.runSingleTest(ast, testCase, agentID, 1)
		}(i, tc)
	}

	wg.Wait()
	return results
}

// runSingleTest runs a single test case
func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID string, runNum int) *Result {
	// Check if this is a dynamic mode test
	if tc.IsDynamicMode() {
		return r.runDynamicTest(ast, tc, agentID)
	}

	// Get input summary for display
	inputSummary := SummarizeInput(tc.Input, 50)
	r.output.TestStart(tc.ID, inputSummary, runNum)

	startTime := time.Now()

	// Create result
	result := &Result{
		ID:       tc.ID,
		Input:    tc.Input,
		Expected: tc.Expected,
		Options:  tc.Options,
	}

	// Execute before script if specified
	var beforeData interface{}
	if tc.Before != "" {
		var err error
		beforeData, err = r.hookExecutor.ExecuteBefore(tc.Before, tc, r.agentPath)
		if err != nil {
			result.Status = StatusError
			result.Error = fmt.Sprintf("before script failed: %s", err.Error())
			result.DurationMs = time.Since(startTime).Milliseconds()
			r.output.TestResult(result.Status, time.Since(startTime))
			r.output.TestError(result.Error)
			// Note: after script is NOT called when before fails
			return result
		}
	}

	// Ensure after script runs even if test fails (but only if before succeeded)
	defer func() {
		if tc.After != "" && (tc.Before == "" || beforeData != nil || result.Status != StatusError || !isBeforeError(result.Error)) {
			if err := r.hookExecutor.ExecuteAfter(tc.After, tc, result, beforeData, r.agentPath); err != nil {
				r.output.Warning("after script failed: %s", err.Error())
			}
		}
	}()

	// Parse input to messages with file loading support
	// BaseDir is derived from the input file directory
	inputOpts := r.getInputOptions()
	messages, err := tc.GetMessagesWithOptions(inputOpts)
	if err != nil {
		result.Status = StatusError
		result.Error = fmt.Sprintf("failed to parse input: %s", err.Error())
		result.DurationMs = time.Since(startTime).Milliseconds()
		r.output.TestResult(result.Status, time.Since(startTime))
		r.output.TestError(result.Error)
		return result
	}

	// Create context
	chatID := GenerateChatID(tc.ID, runNum)
	ctx := NewTestContextFromOptions(chatID, agentID, r.opts, tc)
	defer ctx.Release()

	// Build context options from test case and runner options
	opts := buildContextOptions(tc, r.opts)

	// Create timeout context
	timeout := tc.GetTimeout(r.opts.Timeout)
	timeoutCtx, cancel := stdContext.WithTimeout(ctx.Context, timeout)
	defer cancel()
	ctx.Context = timeoutCtx

	// Run the test
	response, err := ast.Stream(ctx, messages, opts)

	duration := time.Since(startTime)
	result.DurationMs = duration.Milliseconds()

	// Check for timeout
	if timeoutCtx.Err() != nil {
		result.Status = StatusTimeout
		result.Error = fmt.Sprintf("timeout after %s", timeout)
		r.output.TestResult(result.Status, duration)
		r.output.TestError(result.Error)
		return result
	}

	// Check for error
	if err != nil {
		result.Status = StatusError
		result.Error = err.Error()
		r.output.TestResult(result.Status, duration)
		r.output.TestError(result.Error)
		return result
	}

	// Extract output and build trace for diagnostics
	result.Output = extractOutput(response)
	result.Trace = buildTrace(response)

	// Validate result using asserter (with response for tool_called assertions)
	asserter := NewAsserter().WithResponse(response)
	passed, errMsg := asserter.Validate(tc, result.Output)
	if passed {
		result.Status = StatusPassed
	} else {
		result.Status = StatusFailed
		result.Error = errMsg
	}

	r.output.TestResult(result.Status, duration)
	if result.Status == StatusFailed {
		r.output.TestError(result.Error)
	}
	r.output.TestOutput(fmt.Sprintf("%v", result.Output))

	return result
}

// runDynamicTest runs a dynamic (simulator-driven) test case
func (r *Executor) runDynamicTest(ast *assistant.Assistant, tc *Case, agentID string) *Result {
	// Output test start for dynamic mode
	r.output.DynamicTestStart(tc.ID, len(tc.Checkpoints))

	startTime := time.Now()

	// Execute before script if specified
	var beforeData interface{}
	if tc.Before != "" {
		var err error
		beforeData, err = r.hookExecutor.ExecuteBefore(tc.Before, tc, r.agentPath)
		if err != nil {
			result := &Result{
				ID:         tc.ID,
				Status:     StatusError,
				Error:      fmt.Sprintf("before script failed: %s", err.Error()),
				DurationMs: time.Since(startTime).Milliseconds(),
			}
			r.output.TestResult(result.Status, time.Since(startTime))
			r.output.TestError(result.Error)
			return result
		}
	}

	// Create dynamic runner and execute
	dynamicRunner := NewDynamicRunner(r.opts)
	dynamicResult := dynamicRunner.RunDynamic(ast, tc, agentID)

	// Convert to standard result
	result := dynamicResult.ToResult()

	// Execute after script if specified (before outputting result)
	if tc.After != "" && (tc.Before == "" || beforeData != nil || result.Status != StatusError || !isBeforeError(result.Error)) {
		if err := r.hookExecutor.ExecuteAfter(tc.After, tc, result, beforeData, r.agentPath); err != nil {
			r.output.Warning("after script failed: %s", err.Error())
		}
	}

	// Output result
	duration := time.Duration(result.DurationMs) * time.Millisecond
	r.output.DynamicTestResult(result.Status, dynamicResult.TotalTurns, len(tc.Checkpoints), duration)

	if result.Error != "" {
		r.output.TestError(result.Error)
	}

	return result
}

// isBeforeError checks if the error message indicates a before script failure
func isBeforeError(errMsg string) bool {
	return len(errMsg) > 0 && errMsg[:min(len(errMsg), 20)] == "before script failed"
}

// min returns the minimum of two integers
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// runStabilityTests runs each test case multiple times for stability analysis
func (r *Executor) runStabilityTests(ast *assistant.Assistant, testCases []*Case, agentID string) []*StabilityResult {
	results := make([]*StabilityResult, 0, len(testCases))

	for _, tc := range testCases {
		sr := &StabilityResult{
			ID:         tc.ID,
			Input:      tc.Input,
			Expected:   tc.Expected,
			RunDetails: make([]*RunDetail, 0, r.opts.Runs),
		}

		// Run multiple times
		for run := 1; run <= r.opts.Runs; run++ {
			result := r.runSingleTest(ast, tc, agentID, run)

			rd := &RunDetail{
				Run:        run,
				Status:     result.Status,
				DurationMs: result.DurationMs,
				Output:     result.Output,
				Error:      result.Error,
			}
			sr.RunDetails = append(sr.RunDetails, rd)
		}

		// Calculate stability metrics
		sr.CalculateStability()

		// Print stability result
		r.output.StabilityResult(sr)

		results = append(results, sr)

		// Check fail-fast
		if r.opts.FailFast && !sr.Stable {
			r.output.Warning("Stopping due to --fail-fast (unstable test: %s)", tc.ID)
			break
		}
	}

	return results
}

// calculateSingleSummary calculates summary for single run mode
func (r *Executor) calculateSingleSummary(report *Report) {
	for _, result := range report.Results {
		switch result.Status {
		case StatusPassed:
			report.Summary.Passed++
		case StatusFailed:
			report.Summary.Failed++
		case StatusError:
			report.Summary.Errors++
		case StatusTimeout:
			report.Summary.Timeouts++
		}
	}
}

// calculateStabilitySummary calculates summary for stability mode
func (r *Executor) calculateStabilitySummary(report *Report) {
	report.Summary.TotalRuns = len(report.StabilityResults) * r.opts.Runs

	var totalPassRate float64
	for _, sr := range report.StabilityResults {
		if sr.Stable {
			report.Summary.StableCases++
			report.Summary.Passed++
		} else {
			report.Summary.UnstableCases++
			report.Summary.Failed++
		}
		totalPassRate += sr.PassRate
	}

	if len(report.StabilityResults) > 0 {
		report.Summary.OverallPassRate = totalPassRate / float64(len(report.StabilityResults))
	}
}

// writeOutput writes the test report to the output file
func (r *Executor) writeOutput(report *Report) error {
	file, err := os.Create(r.opts.OutputFile)
	if err != nil {
		return fmt.Errorf("failed to create output file: %w", err)
	}
	defer file.Close()

	// Get reporter based on -r flag or file extension
	reporter := GetReporterWithAgent(r.opts.ReporterID, r.opts.OutputFile, r.opts.Verbose)

	// If using agent reporter, set context
	if agentReporter, ok := reporter.(*AgentReporter); ok {
		// Create a context for the reporter agent call
		ctx := NewTestContext("reporter", r.opts.ReporterID, report.Environment)
		defer ctx.Release()
		agentReporter.SetContext(ctx)
	}

	// Write report using the reporter
	return reporter.Write(report, file)
}

// buildContextOptions builds context.Options from test case and runner options
// Priority: test case options > runner options > defaults
func buildContextOptions(tc *Case, runnerOpts *Options) *context.Options {
	opts := &context.Options{
		Skip: &context.Skip{
			History: true, // Default: skip history loading - input already contains full conversation
		},
	}

	// Apply test case options if specified
	if tc.Options != nil {
		// Connector: test case > runner
		if tc.Options.Connector != "" {
			opts.Connector = tc.Options.Connector
		}

		// Mode
		if tc.Options.Mode != "" {
			opts.Mode = tc.Options.Mode
		}

		// DisableGlobalPrompts
		if tc.Options.DisableGlobalPrompts {
			opts.DisableGlobalPrompts = true
		}

		// Search (pointer to distinguish unset from false)
		if tc.Options.Search != nil {
			opts.Search = tc.Options.Search
		}

		// Metadata for hooks
		if tc.Options.Metadata != nil {
			opts.Metadata = tc.Options.Metadata
		}

		// Skip options from test case
		if tc.Options.Skip != nil {
			opts.Skip.Trace = tc.Options.Skip.Trace
			opts.Skip.Output = tc.Options.Skip.Output
			opts.Skip.Keyword = tc.Options.Skip.Keyword
			opts.Skip.Search = tc.Options.Skip.Search
			// Note: History defaults to true for tests
		}
	}

	// Runner connector override (highest priority)
	if runnerOpts != nil && runnerOpts.Connector != "" {
		opts.Connector = runnerOpts.Connector
	}

	return opts
}

// extractOutput extracts the output from the agent response
// Priority: Next hook data > Completion content > Tool results message > nil
func extractOutput(response *context.Response) interface{} {
	if response == nil {
		return nil
	}

	// Prefer Next hook data if available and non-empty
	// response.Next is already the Data value (not NextHookResponse struct)
	if response.Next != nil && !isEmptyValue(response.Next) {
		return response.Next
	}

	// Fall back to completion response
	if response.Completion != nil {
		// If content is non-empty, return it
		if response.Completion.Content != nil && !isEmptyValue(response.Completion.Content) {
			return response.Completion.Content
		}
	}

	// If no content but tools were executed, extract message from tool results
	// This handles the case where LLM calls tools but doesn't generate text
	if len(response.Tools) > 0 {
		return extractToolResultMessage(response.Tools)
	}

	return nil
}

// extractToolResultMessage extracts the message field from tool results
// Returns the first non-empty message found, or a summary of tool calls
func extractToolResultMessage(tools []context.ToolCallResponse) interface{} {
	if len(tools) == 0 {
		return nil
	}

	// Try to extract "message" field from tool results first
	for _, tool := range tools {
		if tool.Result != nil {
			// Try to get message from result map
			if resultMap, ok := tool.Result.(map[string]interface{}); ok {
				if msg, exists := resultMap["message"]; exists && msg != nil {
					if msgStr, ok := msg.(string); ok && msgStr != "" {
						return msgStr
					}
				}
			}
		}
	}

	// No message found, generate a summary of tool calls
	var summaries []string
	for _, tool := range tools {
		toolName := tool.Tool
		if toolName == "" {
			toolName = "unknown"
		}
		// Extract key info from result if possible
		if tool.Result != nil {
			if resultMap, ok := tool.Result.(map[string]interface{}); ok {
				// Try common result fields
				if action, ok := resultMap["action"].(string); ok {
					summaries = append(summaries, fmt.Sprintf("[%s: %s]", toolName, action))
					continue
				}
				if success, ok := resultMap["success"].(bool); ok {
					status := "failed"
					if success {
						status = "success"
					}
					summaries = append(summaries, fmt.Sprintf("[%s: %s]", toolName, status))
					continue
				}
			}
		}
		summaries = append(summaries, fmt.Sprintf("[%s]", toolName))
	}

	if len(summaries) > 0 {
		return strings.Join(summaries, " ")
	}
	return nil
}

// isEmptyValue checks if a value is considered "empty" for output purposes
func isEmptyValue(v interface{}) bool {
	if v == nil {
		return true
	}

	// Use reflection to check for typed nil (e.g., *NextHookResponse(nil))
	rv := reflect.ValueOf(v)
	if rv.Kind() == reflect.Ptr && rv.IsNil() {
		return true
	}

	switch val := v.(type) {
	case string:
		return val == ""
	case map[string]interface{}:
		return len(val) == 0
	case []interface{}:
		return len(val) == 0
	case *context.NextHookResponse:
		// Check if NextHookResponse is effectively empty
		if val == nil {
			return true
		}
		return val.Data == nil && val.Delegate == nil
	}

	return false
}

// buildTrace builds a Trace from the full agent response.
// It merges Completion.ToolCalls (request-side: ID, name, arguments) with
// response.Tools (result-side: server, result, error) by matching ToolCallID,
// so parallel MCP calls are correctly associated.
func buildTrace(response *context.Response) *Trace {
	if response == nil {
		return nil
	}

	trace := &Trace{}

	if response.Completion != nil {
		trace.Completion = &CompletionTrace{
			Model:            response.Completion.Model,
			Role:             response.Completion.Role,
			Content:          response.Completion.Content,
			ReasoningContent: response.Completion.ReasoningContent,
			Refusal:          response.Completion.Refusal,
		}
	}

	// Build tool call index from results keyed by ToolCallID
	resultByID := make(map[string]*context.ToolCallResponse, len(response.Tools))
	for i := range response.Tools {
		if response.Tools[i].ToolCallID != "" {
			resultByID[response.Tools[i].ToolCallID] = &response.Tools[i]
		}
	}

	// Merge request-side (Completion.ToolCalls) with result-side (response.Tools)
	if response.Completion != nil && len(response.Completion.ToolCalls) > 0 {
		for i, tc := range response.Completion.ToolCalls {
			var args interface{}
			if tc.Function.Arguments != "" {
				if err := jsoniter.UnmarshalFromString(tc.Function.Arguments, &args); err != nil {
					args = tc.Function.Arguments
				}
			}
			entry := TraceToolCall{
				ID:        tc.ID,
				Tool:      tc.Function.Name,
				Arguments: args,
			}
			if r, ok := resultByID[tc.ID]; ok {
				entry.Server = r.Server
				entry.Result = r.Result
				entry.Error = r.Error
			} else if i < len(response.Tools) {
				entry.Server = response.Tools[i].Server
				entry.Result = response.Tools[i].Result
				entry.Error = response.Tools[i].Error
			}
			entry.Tool = stripServerPrefix(entry.Tool, entry.Server)
			trace.ToolCalls = append(trace.ToolCalls, entry)
		}
	} else if len(response.Tools) > 0 {
		for _, t := range response.Tools {
			trace.ToolCalls = append(trace.ToolCalls, TraceToolCall{
				ID:        t.ToolCallID,
				Server:    t.Server,
				Tool:      stripServerPrefix(t.Tool, t.Server),
				Arguments: t.Arguments,
				Result:    t.Result,
				Error:     t.Error,
			})
		}
	}

	if response.Next != nil && !isEmptyValue(response.Next) {
		trace.Next = response.Next
	}

	if trace.Completion == nil && len(trace.ToolCalls) == 0 && trace.Next == nil {
		return nil
	}

	return trace
}

// stripServerPrefix removes the "{server}__" prefix from an encoded tool name.
// MCP tools are internally encoded as "server__tool" (e.g. "echo__ping"),
// but for trace output we display the original tool name since server is a separate field.
func stripServerPrefix(tool, server string) string {
	if server == "" {
		return tool
	}
	prefix := server + "__"
	if strings.HasPrefix(tool, prefix) {
		return tool[len(prefix):]
	}
	return tool
}

// validateOutput validates the actual output against expected
func validateOutput(actual, expected interface{}) bool {
	// Simple JSON comparison
	actualJSON, err1 := jsoniter.Marshal(actual)
	expectedJSON, err2 := jsoniter.Marshal(expected)

	if err1 != nil || err2 != nil {
		return false
	}

	return string(actualJSON) == string(expectedJSON)
}

// getInputOptions returns InputOptions based on the runner configuration
// BaseDir is derived from the input file directory (for file mode) or current working directory
func (r *Executor) getInputOptions() *InputOptions {
	opts := &InputOptions{}

	// For file mode, use the input file's directory as base
	if r.opts.InputMode == InputModeFile && r.opts.Input != "" {
		// Resolve path considering YAO_ROOT
		resolvedPath := ResolvePathWithYaoRoot(r.opts.Input)
		opts.BaseDir = filepath.Dir(resolvedPath)
	}
	// For message mode, BaseDir remains empty (uses current working directory)

	return opts
}

// outputDryRun outputs generated test cases without running them
func (r *Executor) outputDryRun(testCases []*Case, agentInfo *AgentInfo) (*Report, error) {
	r.output.Info("Generated Test Cases:")

	// Output each test case as JSONL
	for _, tc := range testCases {
		data, err := jsoniter.Marshal(tc)
		if err != nil {
			r.output.Warning("Failed to marshal test case %s: %s", tc.ID, err.Error())
			continue
		}
		fmt.Println(string(data))
	}

	// Write to output file if specified
	if r.opts.OutputFile != "" {
		file, err := os.Create(r.opts.OutputFile)
		if err != nil {
			return nil, fmt.Errorf("failed to create output file: %w", err)
		}
		defer file.Close()

		for _, tc := range testCases {
			data, err := jsoniter.Marshal(tc)
			if err != nil {
				continue
			}
			file.WriteString(string(data) + "\n")
		}

		r.output.Info("Output written to: %s", r.opts.OutputFile)
	}

	// Return a minimal report
	connector := r.opts.Connector
	if connector == "" {
		connector = agentInfo.Connector
	}

	return &Report{
		Summary: &Summary{
			Total:     len(testCases),
			AgentID:   agentInfo.ID,
			Connector: connector,
		},
	}, nil
}
