package cache_test

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/yaoapp/gou/model"
	"github.com/yaoapp/xun/capsule"
	"github.com/yaoapp/yao/agent/robot/cache"
	"github.com/yaoapp/yao/agent/robot/types"
	"github.com/yaoapp/yao/agent/testutils"
)

// TestCacheLoad tests loading all active robots from database
func TestCacheLoad(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test")
	}

	testutils.Prepare(t)
	defer testutils.Clean(t)

	// Clean up any existing test data first
	cleanupTestRobots(t)

	// Create test robots in database
	setupTestRobots(t)
	defer cleanupTestRobots(t)

	c := cache.New()
	ctx := types.NewContext(context.Background(), nil)

	// Load all robots
	err := c.Load(ctx)
	assert.NoError(t, err)

	// Count should be at least 2 (may have other robots in DB)
	count := c.Count()
	assert.GreaterOrEqual(t, count, 2, "Should load at least 2 active robots")

	// Verify first robot
	robot1 := c.Get("robot_test_sales_001")
	assert.NotNil(t, robot1, "Sales bot should be loaded")
	if robot1 == nil {
		t.Fatal("robot_test_sales_001 not found in cache")
	}
	assert.Equal(t, "robot_test_sales_001", robot1.MemberID)
	assert.Equal(t, "team_test_cache_001", robot1.TeamID)
	assert.Equal(t, "Test Sales Bot", robot1.DisplayName)
	assert.Equal(t, types.RobotIdle, robot1.Status)
	assert.True(t, robot1.AutonomousMode)
	assert.NotNil(t, robot1.Config, "Robot config should be parsed")
	assert.NotNil(t, robot1.Config.Identity, "Identity should be parsed")
	assert.Equal(t, "Sales Manager", robot1.Config.Identity.Role)
	assert.Equal(t, 3, robot1.Config.Quota.GetMax())

	// Verify second robot
	robot2 := c.Get("robot_test_support_002")
	assert.NotNil(t, robot2, "Support bot should be loaded")
	assert.Equal(t, "robot_test_support_002", robot2.MemberID)
	assert.Equal(t, "Test Support Bot", robot2.DisplayName)
	assert.NotNil(t, robot2.Config)
	assert.Equal(t, "Customer Support", robot2.Config.Identity.Role)
	assert.Equal(t, 2, robot2.Config.Quota.GetMax())

	// Verify inactive robot is not loaded
	robot3 := c.Get("robot_test_inactive_003")
	assert.Nil(t, robot3, "Inactive robot should not be loaded")
}

// TestCacheLoadByID tests loading a single robot by member ID
func TestCacheLoadByID(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test")
	}

	testutils.Prepare(t)
	defer testutils.Clean(t)

	cleanupTestRobots(t)
	setupTestRobots(t)
	defer cleanupTestRobots(t)

	c := cache.New()
	ctx := types.NewContext(context.Background(), nil)

	t.Run("load existing robot", func(t *testing.T) {
		robot, err := c.LoadByID(ctx, "robot_test_sales_001")
		assert.NoError(t, err)
		assert.NotNil(t, robot)
		assert.Equal(t, "robot_test_sales_001", robot.MemberID)
		assert.Equal(t, "Test Sales Bot", robot.DisplayName)
		assert.NotNil(t, robot.Config)
	})

	t.Run("load non-existent robot", func(t *testing.T) {
		robot, err := c.LoadByID(ctx, "robot_nonexistent")
		assert.Error(t, err)
		assert.Equal(t, types.ErrRobotNotFound, err)
		assert.Nil(t, robot)
	})

	t.Run("load inactive robot by ID", func(t *testing.T) {
		// LoadByID doesn't filter by status, so it should load
		robot, err := c.LoadByID(ctx, "robot_test_inactive_003")
		assert.NoError(t, err)
		assert.NotNil(t, robot)
		assert.Equal(t, "robot_test_inactive_003", robot.MemberID)
	})
}

// TestCacheRefresh tests refreshing a single robot from database
func TestCacheRefresh(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test")
	}

	testutils.Prepare(t)
	defer testutils.Clean(t)

	cleanupTestRobots(t)
	setupTestRobots(t)
	defer cleanupTestRobots(t)

	c := cache.New()
	ctx := types.NewContext(context.Background(), nil)

	// Load initial data
	err := c.Load(ctx)
	assert.NoError(t, err)

	t.Run("refresh existing robot", func(t *testing.T) {
		err := c.Refresh(ctx, "robot_test_sales_001")
		assert.NoError(t, err)

		// Robot should still be in cache
		robot := c.Get("robot_test_sales_001")
		assert.NotNil(t, robot)
	})

	t.Run("refresh removes non-existent robot", func(t *testing.T) {
		// Add a fake robot to cache
		c.Add(&types.Robot{MemberID: "robot_test_fake", TeamID: "team_test_cache_001"})
		assert.NotNil(t, c.Get("robot_test_fake"))

		// Refresh should remove it
		err := c.Refresh(ctx, "robot_test_fake")
		assert.NoError(t, err)
		assert.Nil(t, c.Get("robot_test_fake"), "Non-existent robot should be removed")
	})
}

// TestCacheListByTeam tests listing robots by team
func TestCacheListByTeam(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test")
	}

	testutils.Prepare(t)
	defer testutils.Clean(t)

	cleanupTestRobots(t)
	setupTestRobots(t)
	defer cleanupTestRobots(t)

	c := cache.New()
	ctx := types.NewContext(context.Background(), nil)

	// Load all robots
	err := c.Load(ctx)
	assert.NoError(t, err)

	// List robots by team
	robots := c.List("team_test_cache_001")
	assert.Len(t, robots, 2, "Should have 2 robots in team_test_cache_001")

	// List robots for non-existent team
	robots = c.List("team_nonexistent")
	assert.Len(t, robots, 0, "Non-existent team should have no robots")
}

// TestCacheGetByStatus tests getting robots by status
func TestCacheGetByStatus(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test")
	}

	testutils.Prepare(t)
	defer testutils.Clean(t)

	cleanupTestRobots(t)
	setupTestRobots(t)
	defer cleanupTestRobots(t)

	c := cache.New()
	ctx := types.NewContext(context.Background(), nil)

	// Load all robots
	err := c.Load(ctx)
	assert.NoError(t, err)

	// Get idle robots (may have others in DB)
	idle := c.GetIdle()
	assert.GreaterOrEqual(t, len(idle), 2, "Should have at least 2 idle robots")

	// Verify our test robots are not working
	testRobot1 := c.Get("robot_test_sales_001")
	testRobot2 := c.Get("robot_test_support_002")
	assert.Equal(t, types.RobotIdle, testRobot1.Status, "Test robot 1 should be idle")
	assert.Equal(t, types.RobotIdle, testRobot2.Status, "Test robot 2 should be idle")
}

// TestCacheAutoRefresh tests auto-refresh functionality and goroutine leak prevention
func TestCacheAutoRefresh(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test")
	}

	testutils.Prepare(t)
	defer testutils.Clean(t)

	cleanupTestRobots(t)
	setupTestRobots(t)
	defer cleanupTestRobots(t)

	// Verify test data is set up
	c := cache.New()
	ctx := types.NewContext(context.Background(), nil)
	err := c.Load(ctx)
	assert.NoError(t, err)
	assert.GreaterOrEqual(t, c.Count(), 1, "Should have at least one robot loaded")

	t.Run("start and stop auto-refresh", func(t *testing.T) {
		// Use a fresh cache for this test
		testCache := cache.New()
		testCtx := types.NewContext(context.Background(), nil)
		err := testCache.Load(testCtx)
		assert.NoError(t, err)

		// Start auto-refresh with short interval
		config := &cache.RefreshConfig{Interval: 100 * time.Millisecond}
		testCache.StartAutoRefresh(testCtx, config)

		// Wait a bit to let it run (should trigger at least 2 refreshes)
		time.Sleep(250 * time.Millisecond)

		// Stop auto-refresh
		testCache.StopAutoRefresh()

		// Verify it stopped by checking that no more refreshes happen
		countBefore := testCache.Count()
		time.Sleep(200 * time.Millisecond)
		countAfter := testCache.Count()

		// Count should be stable (no errors from stopped goroutine)
		assert.Equal(t, countBefore, countAfter, "Cache should be stable after stop")
	})

	t.Run("multiple start calls should replace previous", func(t *testing.T) {
		// Use a fresh cache for this test
		testCache := cache.New()
		testCtx := types.NewContext(context.Background(), nil)
		err := testCache.Load(testCtx)
		assert.NoError(t, err)

		// Track refresh count using a counter
		refreshCount := 0
		originalCount := testCache.Count()

		// Start multiple times without stopping
		config := &cache.RefreshConfig{Interval: 50 * time.Millisecond}

		testCache.StartAutoRefresh(testCtx, config)
		time.Sleep(30 * time.Millisecond)

		testCache.StartAutoRefresh(testCtx, config) // Should stop previous one
		time.Sleep(30 * time.Millisecond)

		testCache.StartAutoRefresh(testCtx, config) // Should stop previous one

		// Wait for some refreshes
		time.Sleep(150 * time.Millisecond)

		// Stop once should be enough
		testCache.StopAutoRefresh()

		// Verify cache still works correctly
		assert.GreaterOrEqual(t, testCache.Count(), 0, "Cache should still be functional")

		// Verify we can still access robots
		_ = refreshCount  // suppress unused warning
		_ = originalCount // suppress unused warning
	})

	t.Run("stop without start should not panic", func(t *testing.T) {
		// Use a fresh cache for this test
		testCache := cache.New()

		// Multiple stops should be safe
		assert.NotPanics(t, func() {
			testCache.StopAutoRefresh()
			testCache.StopAutoRefresh()
			testCache.StopAutoRefresh()
		})
	})

	t.Run("concurrent start and stop should be safe", func(t *testing.T) {
		// Use a fresh cache for this test
		testCache := cache.New()
		testCtx := types.NewContext(context.Background(), nil)
		err := testCache.Load(testCtx)
		assert.NoError(t, err)

		config := &cache.RefreshConfig{Interval: 50 * time.Millisecond}

		// Rapidly start and stop multiple times - should not panic or deadlock
		done := make(chan bool)
		go func() {
			for i := 0; i < 10; i++ {
				testCache.StartAutoRefresh(testCtx, config)
				time.Sleep(10 * time.Millisecond)
				testCache.StopAutoRefresh()
				time.Sleep(10 * time.Millisecond)
			}
			done <- true
		}()

		// Wait with timeout to detect deadlocks
		select {
		case <-done:
			// Success - no deadlock
		case <-time.After(5 * time.Second):
			t.Fatal("Rapid start/stop cycles caused deadlock")
		}

		// Final cleanup
		testCache.StopAutoRefresh()

		// Verify cache is still functional
		assert.GreaterOrEqual(t, testCache.Count(), 0, "Cache should still be functional after rapid cycles")
	})
}

// setupTestRobots creates 3 test robot records in database
func setupTestRobots(t *testing.T) {
	// Get the actual table name from model
	m := model.Select("__yao.member")
	tableName := m.MetaData.Table.Name

	qb := capsule.Query()

	// Robot 1: Sales Bot (active, autonomous)
	robotConfig1 := map[string]interface{}{
		"identity": map[string]interface{}{
			"role":   "Sales Manager",
			"duties": []string{"Manage leads", "Follow up customers"},
			"rules":  []string{"Be professional", "Reply within 24h"},
		},
		"quota": map[string]interface{}{
			"max":      3,
			"queue":    15,
			"priority": 7,
		},
		"clock": map[string]interface{}{
			"mode":  "times",
			"times": []string{"09:00", "14:00"},
			"tz":    "Asia/Shanghai",
		},
	}
	config1JSON, _ := json.Marshal(robotConfig1)

	err := qb.Table(tableName).Insert([]map[string]interface{}{
		{
			"member_id":       "robot_test_sales_001",
			"team_id":         "team_test_cache_001",
			"member_type":     "robot",
			"display_name":    "Test Sales Bot",
			"system_prompt":   "You are a professional sales manager assistant.",
			"status":          "active",
			"role_id":         "member", // required field
			"autonomous_mode": true,
			"robot_status":    "idle",
			"robot_config":    string(config1JSON),
		},
	})
	if err != nil {
		t.Fatalf("Failed to insert robot_test_sales_001: %v", err)
	}

	// Robot 2: Support Bot (active, autonomous)
	robotConfig2 := map[string]interface{}{
		"identity": map[string]interface{}{
			"role":   "Customer Support",
			"duties": []string{"Answer questions", "Resolve issues"},
		},
		"quota": map[string]interface{}{
			"max":      2,
			"queue":    10,
			"priority": 5,
		},
		"clock": map[string]interface{}{
			"mode":  "interval",
			"every": "1h",
		},
	}
	config2JSON, _ := json.Marshal(robotConfig2)

	err = qb.Table(tableName).Insert([]map[string]interface{}{
		{
			"member_id":       "robot_test_support_002",
			"team_id":         "team_test_cache_001",
			"member_type":     "robot",
			"display_name":    "Test Support Bot",
			"system_prompt":   "You are a helpful customer support assistant.",
			"status":          "active",
			"role_id":         "member", // required field
			"autonomous_mode": true,
			"robot_status":    "idle",
			"robot_config":    string(config2JSON),
		},
	})
	if err != nil {
		t.Fatalf("Failed to insert robot_test_support_002: %v", err)
	}

	// Robot 3: Inactive robot (should not be loaded by Load())
	err = qb.Table(tableName).Insert([]map[string]interface{}{
		{
			"member_id":       "robot_test_inactive_003",
			"team_id":         "team_test_cache_001",
			"member_type":     "robot",
			"display_name":    "Test Inactive Bot",
			"status":          "inactive",
			"role_id":         "member", // required field
			"autonomous_mode": true,
			"robot_status":    "paused",
		},
	})
	if err != nil {
		t.Fatalf("Failed to insert robot_test_inactive_003: %v", err)
	}
}

// cleanupTestRobots removes test robot records
func cleanupTestRobots(t *testing.T) {
	qb := capsule.Query()

	// Use the member model to perform soft delete
	m := model.Select("__yao.member")

	// Delete test robots
	m.DeleteWhere(model.QueryParam{
		Wheres: []model.QueryWhere{
			{Column: "member_id", Value: "robot_test_sales_001"},
		},
	})
	m.DeleteWhere(model.QueryParam{
		Wheres: []model.QueryWhere{
			{Column: "member_id", Value: "robot_test_support_002"},
		},
	})
	m.DeleteWhere(model.QueryParam{
		Wheres: []model.QueryWhere{
			{Column: "member_id", Value: "robot_test_inactive_003"},
		},
	})

	// Hard delete from database (cleanup for next test run)
	m2 := model.Select("__yao.member")
	tableName2 := m2.MetaData.Table.Name
	qb.Table(tableName2).Where("member_id", "robot_test_sales_001").Delete()
	qb.Table(tableName2).Where("member_id", "robot_test_support_002").Delete()
	qb.Table(tableName2).Where("member_id", "robot_test_inactive_003").Delete()
}
