package executor

import (
	"os"
	"testing"
	"time"

	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/dag"
)

func TestSetCleanupInterval(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()
	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}

	if exec.cleanupInterval != defaultCleanupInterval {
		t.Fatalf("expected default cleanup interval %v, got %v", defaultCleanupInterval, exec.cleanupInterval)
	}

	newInterval := 30 * time.Minute
	exec.SetCleanupInterval(newInterval)
	if exec.cleanupInterval != newInterval {
		t.Fatalf("expected cleanup interval %v, got %v", newInterval, exec.cleanupInterval)
	}
}

func TestSetKeepDeletedDuration(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()
	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}

	if exec.runningExecTasks.GetKeepDeletedDuration() != defaultKeepDeletedDuration {
		t.Fatalf("expected default keep deleted duration %v, got %v",
			defaultKeepDeletedDuration, exec.runningExecTasks.GetKeepDeletedDuration())
	}

	newDuration := 72 * time.Hour
	exec.SetKeepDeletedDuration(newDuration)
	if exec.runningExecTasks.GetKeepDeletedDuration() != newDuration {
		t.Fatalf("expected keep deleted duration %v, got %v",
			newDuration, exec.runningExecTasks.GetKeepDeletedDuration())
	}

	// Negative duration should fall back to default
	exec.SetKeepDeletedDuration(-1 * time.Hour)
	if exec.runningExecTasks.GetKeepDeletedDuration() != defaultKeepDeletedDuration {
		t.Fatalf("expected default keep deleted duration %v after negative input, got %v",
			defaultKeepDeletedDuration, exec.runningExecTasks.GetKeepDeletedDuration())
	}

	// Zero duration should fall back to default
	exec.SetKeepDeletedDuration(0)
	if exec.runningExecTasks.GetKeepDeletedDuration() != defaultKeepDeletedDuration {
		t.Fatalf("expected default keep deleted duration %v after zero input, got %v",
			defaultKeepDeletedDuration, exec.runningExecTasks.GetKeepDeletedDuration())
	}
}

func TestGetExecStatusStorage(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()
	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}
	stg := exec.GetExecStatusStorage()
	if stg == nil {
		t.Fatal("expected non-nil exec status storage")
	}
}

func TestCleanFinished(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()

	rootName := "test-clean"
	result := NewSafeString()
	actionLib.SafeRegister(&MockBuiltinAction{
		MockActionBase: &actions.MockActionBase{
			ActionName: rootName,
		},
		fn: func() {
			result.Append("done")
		},
	})
	logicPlans.NewPlanWithRootNode(rootName)

	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}

	// Use very short keep duration so we can test cleanup
	exec.SetKeepDeletedDuration(50 * time.Millisecond)

	logger := NewGeneralLogger("TestCleanFinished", os.Stderr)
	exec.RegisterEventReceiver(logger)

	err = exec.Submit(rootName, actions.ActionArgs{}, "exec-clean-1")
	if err != nil {
		t.Fatal(err)
	}
	err = exec.Run(ExitPolicyStopOnFinish)
	if err != nil {
		t.Fatal(err)
	}

	if result.Load() != "done" {
		t.Fatalf("expected 'done' but got '%s'", result.Load())
	}

	// Task should be in the deleted slice after finish (verified via Iterate)
	countDeleted := func() int {
		count := 0
		_ = exec.runningExecTasks.Iterate(true, func(et *ExecutorTask) error {
			count++
			return nil
		})
		return count
	}

	if countDeleted() != 1 {
		t.Fatalf("expected 1 deleted task after finish, got %d", countDeleted())
	}

	// Wait for retention to expire
	time.Sleep(100 * time.Millisecond)

	// Clean should remove expired entries
	cleaned, err := exec.CleanFinished()
	if err != nil {
		t.Fatal(err)
	}

	// After cleanup, deleted list should be empty
	if countDeleted() != 0 {
		t.Fatalf("expected 0 deleted tasks after cleanup, got %d", countDeleted())
	}

	t.Logf("Cleaned %d finished tasks from storage", cleaned)
}

func TestCleanFinishedRetainsRecent(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()

	rootName := "test-retain"
	actionLib.SafeRegister(&MockBuiltinAction{
		MockActionBase: &actions.MockActionBase{
			ActionName: rootName,
		},
		fn: func() {},
	})
	logicPlans.NewPlanWithRootNode(rootName)

	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}

	// Keep for a long time
	exec.SetKeepDeletedDuration(1 * time.Hour)

	logger := NewGeneralLogger("TestCleanFinishedRetainsRecent", os.Stderr)
	exec.RegisterEventReceiver(logger)

	err = exec.Submit(rootName, actions.ActionArgs{}, "exec-retain-1")
	if err != nil {
		t.Fatal(err)
	}
	err = exec.Run(ExitPolicyStopOnFinish)
	if err != nil {
		t.Fatal(err)
	}

	// Count deleted entries via Iterate
	countDeleted := func() int {
		count := 0
		_ = exec.runningExecTasks.Iterate(true, func(et *ExecutorTask) error {
			count++
			return nil
		})
		return count
	}

	// Task should be in deleted list
	if countDeleted() != 1 {
		t.Fatalf("expected 1 deleted task, got %d", countDeleted())
	}

	// Clean should NOT remove recent entries
	_, err = exec.CleanFinished()
	if err != nil {
		t.Fatal(err)
	}

	// Should still be in deleted list because retention not expired
	if countDeleted() != 1 {
		t.Fatalf("expected task to still be in deleted list (retention not expired), got %d", countDeleted())
	}
}

func TestAutoCleanupInRunLoop(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()

	rootName := "test-auto-clean"
	actionLib.SafeRegister(&MockBuiltinAction{
		MockActionBase: &actions.MockActionBase{
			ActionName: rootName,
		},
		fn: func() {},
	})
	logicPlans.NewPlanWithRootNode(rootName)

	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}

	// Very short cleanup interval and retention
	exec.SetCleanupInterval(100 * time.Millisecond)
	exec.SetKeepDeletedDuration(50 * time.Millisecond)
	_ = exec.SetTickerInterval(20 * time.Millisecond)

	logger := NewGeneralLogger("TestAutoCleanupInRunLoop", os.Stderr)
	exec.RegisterEventReceiver(logger)

	err = exec.Start(ExitPolicyNonStop)
	if err != nil {
		t.Fatal(err)
	}

	err = exec.Submit(rootName, actions.ActionArgs{}, "exec-auto-1")
	if err != nil {
		t.Fatal(err)
	}

	// Wait for task to finish and cleanup to trigger
	time.Sleep(500 * time.Millisecond)

	// After auto cleanup, the deleted entry should be cleared
	isDeleted := exec.runningExecTasks.IsDeleted("exec-auto-1")

	err = exec.Stop(ExitPolicyStopNowDontWait)
	if err != nil {
		t.Fatal(err)
	}

	if isDeleted {
		t.Fatal("expected auto cleanup to have removed expired deleted entry")
	}
}

func TestQueryRunningsWithFilter(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()

	rootName := "test-filter"
	actionLib.SafeRegister(&MockBuiltinAction{
		MockActionBase: &actions.MockActionBase{
			ActionName:  rootName,
			MockElapsed: time.Second * 5,
		},
		fn: func() {},
	})
	logicPlans.NewPlanWithRootNode(rootName)

	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}
	_ = exec.SetTickerInterval(10 * time.Millisecond)

	logger := NewGeneralLogger("TestQueryRunningsWithFilter", os.Stderr)
	exec.RegisterEventReceiver(logger)

	err = exec.Start(ExitPolicyNonStop)
	if err != nil {
		t.Fatal(err)
	}

	// Submit tasks with different args
	args1 := actions.ActionArgs{
		"cluster_id": actions.ActionArgString("cluster-a"),
		"pool_id":    actions.ActionArgString("pool-1"),
	}
	args2 := actions.ActionArgs{
		"cluster_id": actions.ActionArgString("cluster-b"),
		"pool_id":    actions.ActionArgString("pool-1"),
	}
	args3 := actions.ActionArgs{
		"cluster_id": actions.ActionArgString("cluster-c"),
		"pool_id":    actions.ActionArgString("pool-2"),
	}

	err = exec.Submit(rootName, args1, "exec-f1")
	if err != nil {
		t.Fatal(err)
	}
	err = exec.Submit(rootName, args2, "exec-f2")
	if err != nil {
		t.Fatal(err)
	}
	err = exec.Submit(rootName, args3, "exec-f3")
	if err != nil {
		t.Fatal(err)
	}

	// Wait for tasks to be queued
	time.Sleep(200 * time.Millisecond)

	// Filter by pool_id=pool-1: should match exec-f1 and exec-f2
	filter1 := NewMatchFilter().Add("pool_id", []string{"pool-1"})
	result1, err := exec.QueryRunningsWithFilter(rootName, filter1)
	if err != nil {
		t.Fatal(err)
	}
	if len(result1) != 2 {
		t.Fatalf("expected 2 tasks matching pool-1, got %d", len(result1))
	}
	if result1["exec-f1"] == nil || result1["exec-f2"] == nil {
		t.Fatalf("expected exec-f1 and exec-f2 in results, got %v", result1)
	}

	// Filter by cluster_id=cluster-c: should match exec-f3 only
	filter2 := NewMatchFilter().Add("cluster_id", []string{"cluster-c"})
	result2, err := exec.QueryRunningsWithFilter(rootName, filter2)
	if err != nil {
		t.Fatal(err)
	}
	if len(result2) != 1 {
		t.Fatalf("expected 1 task matching cluster-c, got %d", len(result2))
	}
	if result2["exec-f3"] == nil {
		t.Fatal("expected exec-f3 in results")
	}

	// Empty filter: should match all 3
	filter3 := NewMatchFilter()
	result3, err := exec.QueryRunningsWithFilter(rootName, filter3)
	if err != nil {
		t.Fatal(err)
	}
	if len(result3) != 3 {
		t.Fatalf("expected 3 tasks with empty filter, got %d", len(result3))
	}

	// Filter by non-existing key: should match 0
	filter4 := NewMatchFilter().Add("non_existing_key", []string{"value"})
	result4, err := exec.QueryRunningsWithFilter(rootName, filter4)
	if err != nil {
		t.Fatal(err)
	}
	if len(result4) != 0 {
		t.Fatalf("expected 0 tasks with non-existing key, got %d", len(result4))
	}

	// Multi-key filter: pool_id=pool-1 AND cluster_id=cluster-a
	filter5 := NewMatchFilter().
		Add("pool_id", []string{"pool-1"}).
		Add("cluster_id", []string{"cluster-a"})
	result5, err := exec.QueryRunningsWithFilter(rootName, filter5)
	if err != nil {
		t.Fatal(err)
	}
	if len(result5) != 1 {
		t.Fatalf("expected 1 task matching pool-1 AND cluster-a, got %d", len(result5))
	}
	if result5["exec-f1"] == nil {
		t.Fatal("expected exec-f1 in results")
	}

	err = exec.Stop(ExitPolicyStopNowDontWait)
	if err != nil {
		t.Fatal(err)
	}
}

func TestQueryExecTasks(t *testing.T) {
	logicPlans := dag.NewPlans()
	actionLib := actions.NewActionLibrary()

	rootName := "test-query"
	actionLib.SafeRegister(&MockBuiltinAction{
		MockActionBase: &actions.MockActionBase{
			ActionName:  rootName,
			MockElapsed: time.Second * 5,
		},
		fn: func() {},
	})
	logicPlans.NewPlanWithRootNode(rootName)

	exec, err := NewExecutor(actionLib, logicPlans, nil, nil, 10)
	if err != nil {
		t.Fatal(err)
	}
	_ = exec.SetTickerInterval(10 * time.Millisecond)

	logger := NewGeneralLogger("TestQueryExecTasks", os.Stderr)
	exec.RegisterEventReceiver(logger)

	err = exec.Start(ExitPolicyNonStop)
	if err != nil {
		t.Fatal(err)
	}

	args := actions.ActionArgs{
		"key1": actions.ActionArgString("val1"),
	}
	err = exec.Submit(rootName, args, "exec-q1")
	if err != nil {
		t.Fatal(err)
	}

	time.Sleep(200 * time.Millisecond)

	// QueryExecTasks with match-all func
	ids, err := exec.QueryExecTasks(rootName, func(a actions.ActionArgs) (bool, error) {
		return true, nil
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(ids) == 0 {
		t.Fatal("expected at least 1 exec task")
	}

	// QueryExecTasks with non-matching func
	ids2, err := exec.QueryExecTasks(rootName, func(a actions.ActionArgs) (bool, error) {
		return false, nil
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(ids2) != 0 {
		t.Fatalf("expected 0 exec tasks, got %d", len(ids2))
	}

	err = exec.Stop(ExitPolicyStopNowDontWait)
	if err != nil {
		t.Fatal(err)
	}
}

func TestSafeExecutorTasksCleanExpired(t *testing.T) {
	// Start with long duration so Delete doesn't auto-clean
	tasks := NewSafeExecutorTasks(1 * time.Hour)

	task1 := &ExecutorTask{ExecId: "t1", RootName: "plan1"}
	task2 := &ExecutorTask{ExecId: "t2", RootName: "plan2"}
	task3 := &ExecutorTask{ExecId: "t3", RootName: "plan3"}

	tasks.Add(task1)
	tasks.Add(task2)
	tasks.Add(task3)

	// Delete all three
	tasks.Delete("t1")
	tasks.Delete("t2")
	tasks.Delete("t3")

	// Verify deleted entries via Iterate
	countDeleted := func() int {
		count := 0
		_ = tasks.Iterate(true, func(et *ExecutorTask) error {
			count++
			return nil
		})
		return count
	}

	// 0 active + 3 deleted
	if countDeleted() != 3 {
		t.Fatalf("expected 3 deleted tasks, got %d", countDeleted())
	}

	// Now shorten the keep duration to very short and wait
	tasks.SetKeepDeletedDuration(10 * time.Millisecond)
	time.Sleep(50 * time.Millisecond)

	// Clean expired - all 3 should be removed
	tasks.CleanExpired()

	if countDeleted() != 0 {
		t.Fatalf("expected 0 deleted tasks after cleanup, got %d", countDeleted())
	}
}

func TestSafeExecutorTasksCleanExpiredPartial(t *testing.T) {
	// Long duration initially
	tasks := NewSafeExecutorTasks(1 * time.Hour)

	task1 := &ExecutorTask{ExecId: "t1", RootName: "plan1"}
	task2 := &ExecutorTask{ExecId: "t2", RootName: "plan2"}

	tasks.Add(task1)
	tasks.Add(task2)

	// Delete t1 first
	tasks.Delete("t1")
	time.Sleep(300 * time.Millisecond)

	// Delete t2 (more recent)
	tasks.Delete("t2")

	// Set keep duration to something between t1 and t2's age.
	// t1 was deleted ~300ms ago, t2 was just deleted.
	// With 150ms keep duration, t1 (>150ms old) should expire, t2 (<150ms old) should remain.
	tasks.SetKeepDeletedDuration(150 * time.Millisecond)
	time.Sleep(50 * time.Millisecond)

	// Now t1 is ~350ms old (> 150ms), t2 is ~50ms old (< 150ms)
	tasks.CleanExpired()

	countDeleted := func() int {
		count := 0
		_ = tasks.Iterate(true, func(et *ExecutorTask) error {
			count++
			return nil
		})
		return count
	}

	// Only t2 should remain
	if countDeleted() != 1 {
		t.Fatalf("expected 1 deleted task after partial cleanup, got %d", countDeleted())
	}
}

func TestSafeExecTaskIdStatusCleanExpired(t *testing.T) {
	keepDuration := 100 * time.Millisecond
	status := NewSafeExecTaskIdStatus(keepDuration)

	task1 := &ExecutorTask{ExecId: "s1", RootName: "plan1"}
	task2 := &ExecutorTask{ExecId: "s2", RootName: "plan2"}

	status.EnsureExisted(task1)
	status.EnsureExisted(task2)

	// Delete s1
	status.Delete("s1", true)

	isDeleting, _ := status.IsDeleting("s1")
	if !isDeleting {
		t.Fatal("s1 should be deleting")
	}

	// Wait for expiry
	time.Sleep(150 * time.Millisecond)

	// Delete s2 (recent)
	status.Delete("s2", true)

	// Clean expired
	status.CleanExpired()

	// s1 should be cleaned
	isDeleting, _ = status.IsDeleting("s1")
	if isDeleting {
		t.Fatal("s1 should have been cleaned (expired)")
	}

	// s2 should still exist
	isDeleting, _ = status.IsDeleting("s2")
	if !isDeleting {
		t.Fatal("s2 should still be deleting (recent)")
	}
}

func TestSafeExecutorTasksSetKeepDeletedDuration(t *testing.T) {
	tasks := NewSafeExecutorTasks(1 * time.Hour)

	if tasks.GetKeepDeletedDuration() != 1*time.Hour {
		t.Fatalf("expected 1h, got %v", tasks.GetKeepDeletedDuration())
	}

	tasks.SetKeepDeletedDuration(30 * time.Minute)
	if tasks.GetKeepDeletedDuration() != 30*time.Minute {
		t.Fatalf("expected 30m, got %v", tasks.GetKeepDeletedDuration())
	}
}

func TestSafeExecTaskIdStatusSetKeepDeletedDuration(t *testing.T) {
	status := NewSafeExecTaskIdStatus(1 * time.Hour)

	// Set a short retention so we can verify cleanup actually uses it.
	status.SetKeepDeletedDuration(100 * time.Millisecond)

	task := &ExecutorTask{ExecId: "test", RootName: "plan"}
	status.EnsureExisted(task)
	status.Delete("test", true)

	isDeleting, _ := status.IsDeleting("test")
	if !isDeleting {
		t.Fatal("should be in deleting state")
	}

	// Before expiry: CleanExpired should keep the entry.
	status.CleanExpired()
	isDeleting, _ = status.IsDeleting("test")
	if !isDeleting {
		t.Fatal("should still be in deleting state before expiry")
	}

	// Wait past the retention window.
	time.Sleep(200 * time.Millisecond)
	status.CleanExpired()
	isDeleting, _ = status.IsDeleting("test")
	if isDeleting {
		t.Fatal("entry should have been cleaned after retention expired")
	}
}

func TestDefaultConstants(t *testing.T) {
	if defaultTickerInterval != 5*time.Second {
		t.Fatalf("expected defaultTickerInterval 5s, got %v", defaultTickerInterval)
	}
	if defaultCleanupInterval != 3*time.Hour {
		t.Fatalf("expected defaultCleanupInterval 3h, got %v", defaultCleanupInterval)
	}
	if defaultKeepDeletedDuration != 48*time.Hour {
		t.Fatalf("expected defaultKeepDeletedDuration 48h, got %v", defaultKeepDeletedDuration)
	}
}
