package executor

import (
	"sync"
	"testing"
	"time"

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

func makeActionTask(name string) *ActionTask {
	return &ActionTask{
		DagAction: dag.NewDagAction(&actions.MockActionBase{ActionName: name}, nil),
		ExecTask: &ExecutorTask{
			ExecId: "test-exec-id",
		},
	}
}

func TestFifoSingleThreadScheduler(t *testing.T) {
	t.Run("empty queue", func(t *testing.T) {
		s := NewFifoSigleThreadScheduler()
		if s.Len() != 0 {
			t.Errorf("empty queue Len() = %d, want 0", s.Len())
		}
		result := s.PopToBeRuns(nil)
		if result != nil {
			t.Errorf("empty queue PopToBeRuns() = %v, want nil", result)
		}
	})

	t.Run("fifo order", func(t *testing.T) {
		s := NewFifoSigleThreadScheduler()
		task1 := makeActionTask("task1")
		task2 := makeActionTask("task2")
		task3 := makeActionTask("task3")

		s.Add(task1, task2, task3)
		if s.Len() != 3 {
			t.Errorf("Len() = %d, want 3", s.Len())
		}

		result := s.PopToBeRuns(nil)
		if len(result) != 1 {
			t.Fatalf("PopToBeRuns() len = %d, want 1", len(result))
		}
		if result[0].DagAction.Action.Name() != "task1" {
			t.Errorf("first task = %s, want task1", result[0].DagAction.Action.Name())
		}

		result = s.PopToBeRuns(nil)
		if len(result) != 1 {
			t.Fatalf("PopToBeRuns() len = %d, want 1", len(result))
		}
		if result[0].DagAction.Action.Name() != "task2" {
			t.Errorf("second task = %s, want task2", result[0].DagAction.Action.Name())
		}

		result = s.PopToBeRuns(nil)
		if len(result) != 1 {
			t.Fatalf("PopToBeRuns() len = %d, want 1", len(result))
		}
		if result[0].DagAction.Action.Name() != "task3" {
			t.Errorf("third task = %s, want task3", result[0].DagAction.Action.Name())
		}

		if s.Len() != 0 {
			t.Errorf("after pop all, Len() = %d, want 0", s.Len())
		}
	})
}

func TestFullSpeedScheduler(t *testing.T) {
	t.Run("empty queue", func(t *testing.T) {
		s := NewFullSpeedScheduler()
		if s.Len() != 0 {
			t.Errorf("empty queue Len() = %d, want 0", s.Len())
		}
		result := s.PopToBeRuns(nil)
		if result != nil {
			t.Errorf("empty queue PopToBeRuns() = %v, want nil", result)
		}
	})

	t.Run("pop all at once", func(t *testing.T) {
		s := NewFullSpeedScheduler()
		task1 := makeActionTask("task1")
		task2 := makeActionTask("task2")
		task3 := makeActionTask("task3")

		s.Add(task1, task2, task3)
		if s.Len() != 3 {
			t.Errorf("Len() = %d, want 3", s.Len())
		}

		result := s.PopToBeRuns(nil)
		if len(result) != 3 {
			t.Errorf("PopToBeRuns() len = %d, want 3", len(result))
		}

		if s.Len() != 0 {
			t.Errorf("after pop, Len() = %d, want 0", s.Len())
		}

		result2 := s.PopToBeRuns(nil)
		if result2 != nil {
			t.Errorf("second PopToBeRuns() = %v, want nil", result2)
		}
	})
}

func TestActionTaskPriorQueueScheduler(t *testing.T) {
	cmpFn := func(a, b *ActionTask) bool {
		return a.DagAction.Action.Name() < b.DagAction.Action.Name()
	}

	t.Run("empty queue len", func(t *testing.T) {
		s := NewActionTaskPriorQueueScheduler(cmpFn, 5)
		if s.Len() != 0 {
			t.Errorf("empty queue Len() = %d, want 0", s.Len())
		}
	})

	t.Run("respect concurrency limit", func(t *testing.T) {
		s := NewActionTaskPriorQueueScheduler(cmpFn, 2)
		task1 := makeActionTask("a")
		task2 := makeActionTask("b")
		task3 := makeActionTask("c")

		s.Add(task1, task2, task3)
		if s.Len() != 3 {
			t.Errorf("Len() = %d, want 3", s.Len())
		}

		running := map[string]*ActionTask{
			"running1": makeActionTask("running"),
		}
		result := s.PopToBeRuns(running)
		if len(result) != 1 {
			t.Errorf("PopToBeRuns() with 1 running should return 1 task (concurrency=2), got %d", len(result))
		}
	})

	t.Run("priority order with matching concurrency", func(t *testing.T) {
		s := NewActionTaskPriorQueueScheduler(cmpFn, 3)
		taskC := makeActionTask("c")
		taskA := makeActionTask("a")
		taskB := makeActionTask("b")

		s.Add(taskC, taskA, taskB)

		result := s.PopToBeRuns(nil)
		if len(result) != 3 {
			t.Fatalf("PopToBeRuns() len = %d, want 3", len(result))
		}

		if result[0].DagAction.Action.Name() != "a" {
			t.Errorf("first task = %s, want a", result[0].DagAction.Action.Name())
		}
		if result[1].DagAction.Action.Name() != "b" {
			t.Errorf("second task = %s, want b", result[1].DagAction.Action.Name())
		}
		if result[2].DagAction.Action.Name() != "c" {
			t.Errorf("third task = %s, want c", result[2].DagAction.Action.Name())
		}
	})
}

func TestActionTaskPriorQueueSchedulerBugs(t *testing.T) {
	cmpFn := func(a, b *ActionTask) bool {
		return a.DagAction.Action.Name() < b.DagAction.Action.Name()
	}

	t.Run("BUG: panic when concurrency > queue size", func(t *testing.T) {
		defer func() {
			if r := recover(); r != nil {
				t.Logf("Confirmed bug: PopToBeRuns panics when concurrency > queue size: %v", r)
			}
		}()

		s := NewActionTaskPriorQueueScheduler(cmpFn, 10)
		taskA := makeActionTask("a")
		taskB := makeActionTask("b")
		s.Add(taskA, taskB)

		s.PopToBeRuns(nil)
		t.Error("Expected panic but got none - bug may have been fixed")
	})

	t.Run("BUG: empty queue pop causes panic", func(t *testing.T) {
		defer func() {
			if r := recover(); r != nil {
				t.Logf("Confirmed bug: PopToBeRuns on empty queue panics: %v", r)
			}
		}()

		s := NewActionTaskPriorQueueScheduler(cmpFn, 5)
		s.PopToBeRuns(nil)
		t.Error("Expected panic but got none - bug may have been fixed")
	})

	t.Run("BUG: no panic when running >= concurrency - underflow prevented", func(t *testing.T) {
		defer func() {
			if r := recover(); r != nil {
				t.Logf("Unexpected panic: %v", r)
			}
		}()

		s := NewActionTaskPriorQueueScheduler(cmpFn, 2)
		task1 := makeActionTask("a")
		s.Add(task1)

		running := map[string]*ActionTask{
			"r1": makeActionTask("r1"),
			"r2": makeActionTask("r2"),
		}

		result := s.PopToBeRuns(running)
		t.Logf("When running >= concurrency, PopToBeRuns returns %d tasks (no panic due to uint underflow protection)", len(result))
	})
}

func TestBackOffScheduler(t *testing.T) {
	t.Run("empty queue", func(t *testing.T) {
		s := NewBackOffScheduler(time.Second)
		if s.Len() != 0 {
			t.Errorf("empty queue Len() = %d, want 0", s.Len())
		}
		result := s.PopToBeRuns(nil)
		if result != nil {
			t.Errorf("empty queue PopToBeRuns() = %v, want nil", result)
		}
	})

	t.Run("first execution immediate", func(t *testing.T) {
		s := NewBackOffScheduler(time.Second)
		task := makeActionTask("task1")

		s.Add(task)
		if s.Len() != 1 {
			t.Errorf("Len() = %d, want 1", s.Len())
		}

		result := s.PopToBeRuns(nil)
		if len(result) != 1 {
			t.Errorf("first PopToBeRuns() len = %d, want 1", len(result))
		}
	})

	t.Run("backoff delay on retry", func(t *testing.T) {
		s := NewBackOffScheduler(time.Second * 10)
		task := makeActionTask("task1")

		s.Add(task)
		_ = s.PopToBeRuns(nil)

		s.Add(task)
		if s.Len() != 1 {
			t.Errorf("after re-add, Len() = %d, want 1", s.Len())
		}

		result := s.PopToBeRuns(nil)
		if len(result) != 0 {
			t.Errorf("immediate retry should be delayed, got %d tasks", len(result))
		}
	})

	t.Run("different tasks not delayed", func(t *testing.T) {
		s := NewBackOffScheduler(time.Second)
		task1 := makeActionTask("task1")
		task2 := makeActionTask("task2")

		s.Add(task1)
		_ = s.PopToBeRuns(nil)

		s.Add(task2)
		result := s.PopToBeRuns(nil)
		if len(result) != 1 {
			t.Errorf("different task should not be delayed, got %d tasks", len(result))
		}
	})

	t.Run("history cleanup", func(t *testing.T) {
		s := NewBackOffScheduler(time.Second)

		for i := 0; i < 10001; i++ {
			task := makeActionTask("task")
			s.Add(task)
		}

		if len(s.history) > 10000 {
			t.Errorf("history should be cleared after 10000 entries, got %d", len(s.history))
		}
	})
}

func TestSafeWrapScheduler(t *testing.T) {
	t.Run("wrap FifoSingleThreadScheduler", func(t *testing.T) {
		inner := NewFifoSigleThreadScheduler()
		s := NewSafeWrapScheduler(inner)

		task1 := makeActionTask("task1")
		task2 := makeActionTask("task2")

		s.Add(task1, task2)
		if s.Len() != 2 {
			t.Errorf("Len() = %d, want 2", s.Len())
		}

		result := s.PopToBeRuns(nil)
		if len(result) != 1 {
			t.Errorf("PopToBeRuns() len = %d, want 1", len(result))
		}
	})

	t.Run("change scheduler", func(t *testing.T) {
		inner1 := NewFifoSigleThreadScheduler()
		s := NewSafeWrapScheduler(inner1)

		task1 := makeActionTask("task1")
		task2 := makeActionTask("task2")

		s.Add(task1, task2)

		result := s.PopToBeRuns(nil)
		if len(result) != 1 {
			t.Errorf("FifoScheduler should return 1 task, got %d", len(result))
		}

		inner2 := NewFullSpeedScheduler()
		s.ChangeScheduler(inner2)

		task3 := makeActionTask("task3")
		task4 := makeActionTask("task4")
		s.Add(task3, task4)

		result = s.PopToBeRuns(nil)
		if len(result) != 2 {
			t.Errorf("FullSpeedScheduler should return 2 tasks, got %d", len(result))
		}
	})

	t.Run("concurrent access", func(t *testing.T) {
		inner := NewFullSpeedScheduler()
		s := NewSafeWrapScheduler(inner)

		var wg sync.WaitGroup
		for i := 0; i < 100; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				task := makeActionTask("task")
				s.Add(task)
				_ = s.Len()
			}()
		}
		wg.Wait()
	})
}

func TestActionTasksClone(t *testing.T) {
	tasks := ActionTasks{
		makeActionTask("task1"),
		makeActionTask("task2"),
	}

	cloned := tasks.Clone()
	if len(cloned) != len(tasks) {
		t.Errorf("Clone() len = %d, want %d", len(cloned), len(tasks))
	}

	for i, task := range tasks {
		if cloned[i].DagAction.Action.Name() != task.DagAction.Action.Name() {
			t.Errorf("Clone()[%d] = %s, want %s", i, cloned[i].DagAction.Action.Name(), task.DagAction.Action.Name())
		}
	}
}
