package jobqueue

import (
	"errors"
	"fmt"
	"math/rand"
	"testing"
	"time"
)

func TestEnqueueJobAlreadyExists(t *testing.T) {
	runner := &DummyRunnable{}
	job := NewJob[DummyResult](JobID("1"), runner)
	q := NewQueue(nil)

	_ = q.Enqueue(job)
	err := q.Enqueue(job)
	if err != ErrJobAlreadyExists {
		t.Fatalf("Expected ErrJobAlreadyExists, got %v", err)
	}
}

func TestEnqueueAndDequeue(t *testing.T) {

	runner := &DummyRunnable{}

	q := NewQueue(nil)
	job1 := NewJob[DummyResult](JobID("1"), runner)
	job1.SetPriority(PriorityHigh)
	job2 := NewJob[DummyResult](JobID("2"), runner)
	_ = q.Enqueue(job1)
	_ = q.Enqueue(job2)
	dequeuedJob, err := q.Dequeue()
	if err != nil || dequeuedJob.GetID() != JobID("1") {
		t.Fatalf("Unexpected dequeue result: jobID %s, err %v", dequeuedJob.GetID(), err)
	}
}

func TestEnqueueAndDequeue2(t *testing.T) {

	runner := &DummyRunnable{}

	q := NewQueue(nil)
	job1 := NewJob[DummyResult](JobID("1"), runner)
	job2 := NewJob[DummyResult](JobID("2"), runner)

	job2.AddDependency(JobID("1"))

	_ = q.Enqueue(job1)
	_ = q.Enqueue(job2)
	dequeuedJob, err := q.Dequeue()
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if dequeuedJob.GetID() != JobID("1") {
		t.Fatalf("Unexpected dequeue result: jobID %s", dequeuedJob.GetID())
	}
}

func TestDependencyResolution(t *testing.T) {
	runner := &DummyRunnable{}
	q := NewQueue(nil)
	job1 := NewJob[DummyResult](JobID("1"), runner)
	job2 := NewJob[DummyResult](JobID("2"), runner)
	job3 := NewJob[DummyResult](JobID("3"), runner)

	_ = q.Enqueue(job3)
	_ = q.Enqueue(job2)
	_ = q.Enqueue(job1)

	contains := func(arr []JobID, id JobID) bool {
		for _, v := range arr {
			if v == id {
				return true
			}
		}
		return false
	}

	possibleJobIDs := []JobID{"1", "2", "3"}

	job, _ := q.Dequeue()
	if !contains(possibleJobIDs, job.GetID()) {
		t.Fatalf("Expected jobID in %v, got %s", possibleJobIDs, job.GetID())
	}

	// remove jobID from possibleJobIDs
	for i, v := range possibleJobIDs {
		if v == job.GetID() {
			possibleJobIDs = append(possibleJobIDs[:i], possibleJobIDs[i+1:]...)
		}
	}

	job, _ = q.Dequeue()
	if !contains(possibleJobIDs, job.GetID()) {
		t.Fatalf("Expected jobID in %v, got %s", possibleJobIDs, job.GetID())
	}

	// remove jobID from possibleJobIDs
	for i, v := range possibleJobIDs {
		if v == job.GetID() {
			possibleJobIDs = append(possibleJobIDs[:i], possibleJobIDs[i+1:]...)
		}
	}

	job, _ = q.Dequeue()
	if !contains(possibleJobIDs, job.GetID()) {
		t.Fatalf("Expected jobID in %v, got %s", possibleJobIDs, job.GetID())
	}

	// remove jobID from possibleJobIDs
	for i, v := range possibleJobIDs {
		if v == job.GetID() {
			possibleJobIDs = append(possibleJobIDs[:i], possibleJobIDs[i+1:]...)
		}
	}

	if len(possibleJobIDs) != 0 {
		t.Fatalf("Expected no jobIDs left in %v", possibleJobIDs)
	}

}

func TestDequeueEmptyQueue(t *testing.T) {
	q := NewQueue(nil)
	_, err := q.Dequeue()
	if err != ErrQueueEmpty {
		t.Fatalf("Expected ErrQueueEmpty, got %v", err)
	}
}

func TestProcessedJobs(t *testing.T) {
	q := NewQueue(nil)
	runner := &DummyRunnable{}
	job1 := NewJob[DummyResult](JobID("1"), runner)
	job2 := NewJob[DummyResult](JobID("2"), runner)

	_ = q.Enqueue(job1)
	_, _ = q.Dequeue()
	_ = q.Enqueue(job2)

	_, err := q.Dequeue()
	if err != nil {
		t.Fatalf("Dequeue failed: %v", err)
	}

	if _, exists := q.processedJobs[job1.GetID()]; !exists {
		t.Fatalf("Job 1 not marked as processed")
	}
}

func TestCyclicDependencies(t *testing.T) {
	runner := &DummyRunnable{}
	q := NewQueue(nil)
	job1 := NewJob[DummyResult](JobID("1"), runner)
	job2 := NewJob[DummyResult](JobID("2"), runner)
	job3 := NewJob[DummyResult](JobID("3"), runner)

	job1.AddDependency(JobID("2"))
	job2.AddDependency(JobID("3"))
	job3.AddDependency(JobID("1"))

	err := q.Enqueue(job1)
	if err != nil {
		t.Fatalf("Enqueue failed: %v", err)
	}

	err = q.Enqueue(job2)
	if err != nil {
		t.Fatalf("Enqueue failed: %v", err)
	}

	err = q.Enqueue(job3)
	if err == nil || err != ErrCycleDetected {
		t.Fatalf("Expected ErrCyclicDependency, got %v", err)
	}
}

func TestDuplicateDependencies(t *testing.T) {
	runner := &DummyRunnable{}
	q := NewQueue(nil)
	job1 := NewJob[DummyResult](JobID("1"), runner)
	job2 := NewJob[DummyResult](JobID("2"), runner)

	job2.AddDependency(JobID("1"))
	job2.AddDependency(JobID("1"))

	_ = q.Enqueue(job1)
	err := q.Enqueue(job2)
	if err != nil {
		t.Fatalf("Enqueue failed: %v", err)
	}

	_, err = q.Dequeue()
	if err != nil {
		t.Fatalf("Dequeue failed: %v", err)
	}

	_, err = q.Dequeue()
	if err != nil {
		t.Fatalf("Dequeue failed: %v", err)
	}

	if len(q.processedJobs) != 2 {
		t.Fatalf("Expected 2 processed jobs, got %d", len(q.processedJobs))
	}

}

func TestJobWithSelfAsDependency(t *testing.T) {
	runner := &DummyRunnable{}
	q := NewQueue(nil)
	job1 := NewJob[DummyResult](JobID("1"), runner)

	job1.AddDependency(JobID("1"))

	err := q.Enqueue(job1)
	if err == nil || err != ErrCycleDetected {
		t.Fatalf("Expected ErrCycleDetected, got %v", err)
	}
}

func TestEnqueueDequeue(t *testing.T) {
	q := NewQueue(nil)
	job1 := NewJob[DummyResult]("job1", nil)
	job2 := NewJob[DummyResult]("job2", nil)
	job3 := NewJob[DummyResult]("job3", nil)

	job2.dependencies = []JobID{"job1"}
	job3.dependencies = []JobID{"job2"}

	// Enqueue a job with dependencies that haven't been enqueued yet
	if err := q.Enqueue(job3); err != nil {
		t.Fatalf("Failed to enqueue job3: %v", err)
	}

	// Try to dequeue from an empty readyQueue
	if _, err := q.Dequeue(); !errors.Is(err, ErrQueueEmpty) {
		t.Fatalf("Expected ErrQueueEmpty, got: %v", err)
	}

	// Enqueue a job with no dependencies
	if err := q.Enqueue(job1); err != nil {
		t.Fatalf("Failed to enqueue job1: %v", err)
	}

	// Enqueue a job that has its dependencies met
	if err := q.Enqueue(job2); err != nil {
		t.Fatalf("Failed to enqueue job2: %v", err)
	}

	// Try to dequeue jobs in the expected order
	if job, err := q.Dequeue(); err != nil || job.GetID() != "job1" {
		t.Fatalf("Expected job1, got: %v, %v", job, err)
	}

	if job, err := q.Dequeue(); err != nil || job.GetID() != "job2" {
		t.Fatalf("Expected job2, got: %v, %v", job, err)
	}

	if job, err := q.Dequeue(); err != nil || job.GetID() != "job3" {
		t.Fatalf("Expected job3, got: %v, %v", job, err)
	}
}

func TestDuplicateEnqueue(t *testing.T) {
	q := NewQueue(nil)
	job1 := NewJob[DummyResult]("job1", nil)

	// Enqueue the same job twice
	if err := q.Enqueue(job1); err != nil {
		t.Fatalf("Failed to enqueue job1: %v", err)
	}
	if err := q.Enqueue(job1); !errors.Is(err, ErrJobAlreadyExists) {
		t.Fatalf("Expected ErrJobAlreadyExists, got: %v", err)
	}
}

func TestCircularDependency(t *testing.T) {
	q := NewQueue(nil)

	job1 := NewJob[DummyResult]("job1", nil)
	job2 := NewJob[DummyResult]("job2", nil)

	job1.dependencies = []JobID{"job2"}
	job2.dependencies = []JobID{"job1"}

	if err := q.Enqueue(job1); err != nil {
		t.Fatalf("Failed to enqueue job1: %v", err)
	}
	// This enqueue should fail if you have logic to detect circular dependencies
	if err := q.Enqueue(job2); err == nil {
		t.Fatalf("Expected an error due to circular dependency, got: %v", err)
	}
}

// TestFuzzyEnqueueDequeue tests the queue by generating random jobs and
// you can run it with:
// go test -v -fuzz TestFuzzyEnqueueDequeue
// or with a fuzztime of 10 seconds:
// go test -v -run -fuzz TestFuzzyEnqueueDequeue -fuzztime=10s
func TestFuzzyEnqueueDequeue(t *testing.T) {
	rand.Seed(time.Now().UnixNano())
	q := NewQueue(nil)

	// Generate random jobs
	jobCount := 1000
	jobs := make([]GenericJob, jobCount)
	for i := 0; i < jobCount; i++ {
		jobID := JobID(fmt.Sprintf("job%d", i))
		jobs[i] = NewJob[DummyResult](jobID, nil)
	}

	// Randomly set dependencies for each job
	for _, job := range jobs {
		depCount := rand.Intn(5) // up to 5 dependencies
		deps := make([]JobID, depCount)
		for i := 0; i < depCount; i++ {
			depIndex := rand.Intn(jobCount)
			deps[i] = jobs[depIndex].GetID()
		}
		job.(*Job[DummyResult]).dependencies = deps
	}

	// Randomly enqueue jobs
	for i := 0; i < jobCount; i++ {
		index := rand.Intn(len(jobs))
		job := jobs[index]
		q.Enqueue(job)
		jobs = append(jobs[:index], jobs[index+1:]...)
	}

	// Randomly dequeue jobs, ignoring errors for this fuzzy test
	for i := 0; i < jobCount; i++ {
		_, _ = q.Dequeue()
	}

	// Verify that the queue is empty
	if _, err := q.Dequeue(); !errors.Is(err, ErrQueueEmpty) {
		t.Fatalf("Expected ErrQueueEmpty, got: %v", err)
	}
}

func TestEnqueueDequeueOrder(t *testing.T) {
	eventBus := NewEventBus() // Ihre EventBus-Initialisierung
	q := NewQueue(eventBus)

	job1 := NewJob[DummyResult]("job1", nil)
	job2 := NewJob[DummyResult]("job2", nil)
	job3 := NewJob[DummyResult]("job3", nil)

	job2.AddDependency(JobID("job1"))
	job3.AddDependency(JobID("job2"))

	if err := q.Enqueue(job1); err != nil {
		t.Fatalf("Failed to enqueue job1: %s", err)
	}

	if err := q.Enqueue(job3); err != nil {
		t.Fatalf("Failed to enqueue job3: %s", err)
	}

	if err := q.Enqueue(job2); err != nil {
		t.Fatalf("Failed to enqueue job2: %s", err)
	}

	dequeuedJob, err := q.Dequeue()
	if err != nil {
		t.Fatalf("Failed to dequeue: %s", err)
	}

	if dequeuedJob.GetID() != "job1" {
		t.Errorf("Expected job1, got %s", dequeuedJob.GetID())
	}

	dequeuedJob, err = q.Dequeue()
	if err != nil {
		t.Fatalf("Failed to dequeue: %s", err)
	}

	if dequeuedJob.GetID() != "job2" {
		t.Errorf("Expected job2, got %s", dequeuedJob.GetID())
	}

	dequeuedJob, err = q.Dequeue()
	if err != nil {
		t.Fatalf("Failed to dequeue: %s", err)
	}

	if dequeuedJob.GetID() != "job3" {
		t.Errorf("Expected job3, got %s", dequeuedJob.GetID())
	}
}