package pool_test

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/yaoapp/yao/agent/robot/executor"
	"github.com/yaoapp/yao/agent/robot/pool"
	"github.com/yaoapp/yao/agent/robot/types"
)

// createTestRobot creates a robot for testing with specified quota
func createTestRobot(memberID, teamID string, maxConcurrent, queueSize, priority int) *types.Robot {
	return &types.Robot{
		MemberID:       memberID,
		TeamID:         teamID,
		DisplayName:    "Test Robot " + memberID,
		Status:         types.RobotIdle,
		AutonomousMode: true,
		Config: &types.Config{
			Identity: &types.Identity{Role: "Test"},
			Quota: &types.Quota{
				Max:      maxConcurrent,
				Queue:    queueSize,
				Priority: priority,
			},
		},
	}
}

// createTestContext creates a context for testing
func createTestContext() *types.Context {
	return types.NewContext(context.Background(), nil)
}

// TestPoolStartStop tests pool start and stop lifecycle
func TestPoolStartStop(t *testing.T) {
	p := pool.New()
	exec := executor.New()
	p.SetExecutor(exec)

	t.Run("start pool", func(t *testing.T) {
		err := p.Start()
		assert.NoError(t, err)
		assert.True(t, p.IsStarted())
	})

	t.Run("start already started pool", func(t *testing.T) {
		err := p.Start()
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "already started")
	})

	t.Run("stop pool", func(t *testing.T) {
		err := p.Stop()
		assert.NoError(t, err)
		assert.False(t, p.IsStarted())
	})

	t.Run("stop already stopped pool", func(t *testing.T) {
		err := p.Stop()
		assert.NoError(t, err) // should not error
	})
}

// TestPoolSubmitWithoutStart tests submitting to unstarted pool
func TestPoolSubmitWithoutStart(t *testing.T) {
	p := pool.New()
	exec := executor.New()
	p.SetExecutor(exec)

	robot := createTestRobot("robot_1", "team_1", 2, 10, 5)
	ctx := createTestContext()

	_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not started")
}

// TestPoolSubmitNilRobot tests submitting nil robot
func TestPoolSubmitNilRobot(t *testing.T) {
	p := pool.New()
	exec := executor.New()
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()
	_, err := p.Submit(ctx, nil, types.TriggerClock, nil)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "cannot be nil")
}

// TestPoolBasicExecution tests basic job execution
func TestPoolBasicExecution(t *testing.T) {
	exec := executor.NewDryRunWithDelay(50 * time.Millisecond)
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 5,
		QueueSize:  100,
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	robot := createTestRobot("robot_1", "team_1", 2, 10, 5)
	ctx := createTestContext()

	// Submit a job
	execID, err := p.Submit(ctx, robot, types.TriggerClock, nil)
	assert.NoError(t, err)
	assert.NotEmpty(t, execID)

	// Wait for execution (worker polls every 100ms + 50ms exec + buffer)
	time.Sleep(300 * time.Millisecond)

	// Verify execution completed
	assert.Equal(t, 1, exec.ExecCount())
	// Note: CurrentCount may briefly be non-zero during execution, use Eventually pattern
	assert.Eventually(t, func() bool {
		return exec.CurrentCount() == 0
	}, 500*time.Millisecond, 50*time.Millisecond, "CurrentCount should be 0 after execution")
}

// TestPoolConcurrencyLimit tests global worker limit
func TestPoolConcurrencyLimit(t *testing.T) {
	exec := executor.NewDryRunWithDelay(200 * time.Millisecond) // longer delay
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 3, // only 3 workers
		QueueSize:  100,
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()

	// Create robots with high quota (won't be the bottleneck)
	robots := make([]*types.Robot, 10)
	for i := 0; i < 10; i++ {
		robots[i] = createTestRobot(
			"robot_"+string(rune('A'+i)),
			"team_1",
			5,  // max concurrent per robot
			20, // queue size per robot
			5,  // priority
		)
	}

	// Submit 10 jobs
	for i := 0; i < 10; i++ {
		_, err := p.Submit(ctx, robots[i], types.TriggerClock, nil)
		assert.NoError(t, err)
	}

	// Wait for workers to pick up jobs (worker polls every 100ms)
	time.Sleep(200 * time.Millisecond)

	// Should have at most 3 running (worker limit)
	running := p.Running()
	assert.LessOrEqual(t, running, 3, "Should not exceed worker limit")

	// Wait for all to complete (10 jobs / 3 workers * 200ms each = ~700ms + buffer)
	// Use Eventually to handle CI timing variations
	assert.Eventually(t, func() bool {
		return exec.ExecCount() >= 10
	}, 2*time.Second, 100*time.Millisecond, "All 10 jobs should complete")
}

// TestRobotConcurrencyLimit tests per-robot concurrent execution limit
func TestRobotConcurrencyLimit(t *testing.T) {
	exec := executor.NewDryRunWithDelay(100 * time.Millisecond)
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 10, // plenty of workers
		QueueSize:  100,
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()

	// Create robot with Max=2 (can only run 2 at a time)
	robot := createTestRobot("robot_limited", "team_1", 2, 20, 5)

	// Submit 5 jobs for the same robot
	for i := 0; i < 5; i++ {
		_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
		assert.NoError(t, err)
	}

	// Wait a bit for execution to start
	time.Sleep(150 * time.Millisecond)

	// Robot should have at most 2 running (Quota.Max=2)
	runningCount := robot.RunningCount()
	assert.LessOrEqual(t, runningCount, 2, "Robot should not exceed Quota.Max")

	// Wait for all to complete (with re-enqueue, need more time)
	// 5 jobs with Max=2: ~3 batches * 100ms exec + poll overhead
	time.Sleep(800 * time.Millisecond)

	// All 5 jobs should eventually execute
	assert.GreaterOrEqual(t, exec.ExecCount(), 5, "All jobs should eventually execute")
}

// TestRobotQueueLimit tests per-robot queue limit
func TestRobotQueueLimit(t *testing.T) {
	exec := executor.NewDryRunWithDelay(200 * time.Millisecond)
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 2,
		QueueSize:  100, // global queue is large
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()

	// Create robot with small queue limit
	robot := createTestRobot("robot_small_queue", "team_1", 1, 3, 5) // Queue=3

	// Submit jobs until queue limit is reached
	successCount := 0
	for i := 0; i < 10; i++ {
		_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
		if err == nil {
			successCount++
		}
	}

	// Should only accept up to Queue limit (some may have started executing)
	// Max accepted = Queue(3) + Max(1) = 4 (1 running + 3 in queue)
	assert.LessOrEqual(t, successCount, 4, "Should respect robot queue limit")
	assert.GreaterOrEqual(t, successCount, 1, "Should accept at least 1 job")
}

// TestGlobalQueueLimit tests global queue limit
func TestGlobalQueueLimit(t *testing.T) {
	exec := executor.NewDryRunWithDelay(500 * time.Millisecond) // slow execution
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 1, // only 1 worker
		QueueSize:  5, // small global queue
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()

	// Create multiple robots with large queue limits
	successCount := 0
	for i := 0; i < 20; i++ {
		robot := createTestRobot(
			"robot_"+string(rune('A'+i%26)),
			"team_1",
			5,  // large max
			20, // large per-robot queue
			5,
		)
		_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
		if err == nil {
			successCount++
		}
	}

	// Should only accept up to global queue limit + running
	// Max = QueueSize(5) + WorkerSize(1) = 6
	assert.LessOrEqual(t, successCount, 6, "Should respect global queue limit")
}

// TestPriorityOrder tests that higher priority jobs execute first
func TestPriorityOrder(t *testing.T) {
	exec := executor.NewDryRunWithDelay(50 * time.Millisecond)
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 1, // single worker to ensure order
		QueueSize:  100,
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()

	// Create robots with different priorities
	robotLow := createTestRobot("robot_low", "team_1", 5, 20, 1)    // priority 1
	robotMed := createTestRobot("robot_med", "team_1", 5, 20, 5)    // priority 5
	robotHigh := createTestRobot("robot_high", "team_1", 5, 20, 10) // priority 10

	// Submit in low-to-high order
	p.Submit(ctx, robotLow, types.TriggerClock, nil)
	p.Submit(ctx, robotMed, types.TriggerClock, nil)
	p.Submit(ctx, robotHigh, types.TriggerClock, nil)

	// Wait for all to complete (3 jobs * (100ms poll + 50ms exec) = ~450ms + buffer)
	// Use Eventually for CI timing variations
	assert.Eventually(t, func() bool {
		return exec.ExecCount() >= 3
	}, 1*time.Second, 50*time.Millisecond, "All 3 jobs should complete")
}

// TestTriggerTypePriority tests that human triggers have higher priority than clock
func TestTriggerTypePriority(t *testing.T) {
	exec := executor.NewDryRunWithDelay(50 * time.Millisecond)
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 1, // single worker
		QueueSize:  100,
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()

	// Same robot, same priority, different trigger types
	robot := createTestRobot("robot_1", "team_1", 5, 20, 5)

	// Submit clock first, then human
	p.Submit(ctx, robot, types.TriggerClock, nil)
	p.Submit(ctx, robot, types.TriggerHuman, nil) // should execute first

	// Wait for all to complete (2 jobs * (100ms poll + 50ms exec) = ~300ms + buffer)
	assert.Eventually(t, func() bool {
		return exec.ExecCount() >= 2
	}, 1*time.Second, 50*time.Millisecond, "Both jobs should complete")
}

// TestMultipleRobotsFairness tests that multiple robots get fair access
func TestMultipleRobotsFairness(t *testing.T) {
	exec := executor.NewDryRunWithDelay(30 * time.Millisecond)
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 5,
		QueueSize:  100,
	})
	p.SetExecutor(exec)
	p.Start()
	defer p.Stop()

	ctx := createTestContext()

	// Create 3 robots with same priority
	robotA := createTestRobot("robot_A", "team_1", 2, 10, 5)
	robotB := createTestRobot("robot_B", "team_1", 2, 10, 5)
	robotC := createTestRobot("robot_C", "team_1", 2, 10, 5)

	// Submit jobs for each robot
	for i := 0; i < 6; i++ {
		p.Submit(ctx, robotA, types.TriggerClock, nil)
		p.Submit(ctx, robotB, types.TriggerClock, nil)
		p.Submit(ctx, robotC, types.TriggerClock, nil)
	}

	// Wait for all to complete
	// 18 jobs with Quota.Max=2 per robot, 5 workers, 30ms each
	// Jobs are batched by robot quota, use Eventually for CI timing
	assert.Eventually(t, func() bool {
		return exec.ExecCount() >= 18
	}, 3*time.Second, 100*time.Millisecond, "All 18 jobs should complete")
}

// TestGracefulShutdown tests that pool waits for running jobs on shutdown
func TestGracefulShutdown(t *testing.T) {
	exec := executor.NewDryRunWithDelay(200 * time.Millisecond)
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 2,
		QueueSize:  10,
	})
	p.SetExecutor(exec)
	p.Start()

	ctx := createTestContext()
	robot := createTestRobot("robot_1", "team_1", 5, 20, 5)

	// Submit 2 jobs
	p.Submit(ctx, robot, types.TriggerClock, nil)
	p.Submit(ctx, robot, types.TriggerClock, nil)

	// Wait for workers to pick up jobs (poll every 100ms)
	time.Sleep(150 * time.Millisecond)

	// Verify jobs are running
	assert.GreaterOrEqual(t, p.Running(), 1, "Should have at least 1 running job")

	// Stop - workers will finish their current tick cycle
	p.Stop()

	// After stop, verify jobs completed
	assert.GreaterOrEqual(t, exec.ExecCount(), 1, "Should have executed at least 1 job")
}

// TestDefaultConfig tests default configuration values
func TestDefaultConfig(t *testing.T) {
	config := pool.DefaultConfig()
	assert.Equal(t, pool.DefaultWorkerSize, config.WorkerSize)
	assert.Equal(t, pool.DefaultQueueSize, config.QueueSize)
}

// TestPoolWithNilConfig tests pool creation with nil config
func TestPoolWithNilConfig(t *testing.T) {
	p := pool.NewWithConfig(nil)
	assert.Equal(t, pool.DefaultWorkerSize, p.Size())
	assert.Equal(t, pool.DefaultQueueSize, p.QueueSize())
}

// TestPoolWithZeroConfig tests pool creation with zero values
func TestPoolWithZeroConfig(t *testing.T) {
	p := pool.NewWithConfig(&pool.Config{
		WorkerSize: 0,
		QueueSize:  0,
	})
	// Should use defaults for zero values
	assert.Equal(t, pool.DefaultWorkerSize, p.Size())
	assert.Equal(t, pool.DefaultQueueSize, p.QueueSize())
}

// TestPoolWithoutExecutor tests starting pool without executor
func TestPoolWithoutExecutor(t *testing.T) {
	p := pool.New()
	// Don't set executor
	err := p.Start()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "executor not set")
}
