package scheduler

import (
	"sync"
	"testing"
	"time"

	"github.com/google/uuid"
)

func TestPriorityQueue_BasicOperations(t *testing.T) {
	pq := NewPriorityQueue()

	// Test empty queue
	if pq.Len() != 0 {
		t.Fatalf("expected empty queue, got len=%d", pq.Len())
	}

	if pq.Peek() != nil {
		t.Fatal("Peek on empty queue should return nil")
	}

	if pq.Pop() != nil {
		t.Fatal("Pop on empty queue should return nil")
	}

	// Push a job
	agent1 := uuid.New()
	job1 := &SubsystemJob{
		AgentID:         agent1,
		AgentHostname:   "agent-01",
		Subsystem:       "updates",
		IntervalMinutes: 15,
		NextRunAt:       time.Now().Add(10 * time.Minute),
	}
	pq.Push(job1)

	if pq.Len() != 1 {
		t.Fatalf("expected len=1 after push, got %d", pq.Len())
	}

	// Peek should return the job without removing it
	peeked := pq.Peek()
	if peeked == nil {
		t.Fatal("Peek should return job")
	}
	if peeked.AgentID != agent1 {
		t.Fatal("Peek returned wrong job")
	}
	if pq.Len() != 1 {
		t.Fatal("Peek should not remove job")
	}

	// Pop should return and remove the job
	popped := pq.Pop()
	if popped == nil {
		t.Fatal("Pop should return job")
	}
	if popped.AgentID != agent1 {
		t.Fatal("Pop returned wrong job")
	}
	if pq.Len() != 0 {
		t.Fatal("Pop should remove job")
	}
}

func TestPriorityQueue_Ordering(t *testing.T) {
	pq := NewPriorityQueue()
	now := time.Now()

	// Push jobs in random order
	jobs := []*SubsystemJob{
		{
			AgentID:   uuid.New(),
			Subsystem: "updates",
			NextRunAt: now.Add(30 * time.Minute), // Third
		},
		{
			AgentID:   uuid.New(),
			Subsystem: "storage",
			NextRunAt: now.Add(5 * time.Minute), // First
		},
		{
			AgentID:   uuid.New(),
			Subsystem: "docker",
			NextRunAt: now.Add(15 * time.Minute), // Second
		},
	}

	for _, job := range jobs {
		pq.Push(job)
	}

	// Pop should return jobs in NextRunAt order
	first := pq.Pop()
	if first.Subsystem != "storage" {
		t.Fatalf("expected 'storage' first, got '%s'", first.Subsystem)
	}

	second := pq.Pop()
	if second.Subsystem != "docker" {
		t.Fatalf("expected 'docker' second, got '%s'", second.Subsystem)
	}

	third := pq.Pop()
	if third.Subsystem != "updates" {
		t.Fatalf("expected 'updates' third, got '%s'", third.Subsystem)
	}
}

func TestPriorityQueue_UpdateExisting(t *testing.T) {
	pq := NewPriorityQueue()
	agentID := uuid.New()
	now := time.Now()

	// Push initial job
	job1 := &SubsystemJob{
		AgentID:         agentID,
		Subsystem:       "updates",
		IntervalMinutes: 15,
		NextRunAt:       now.Add(15 * time.Minute),
	}
	pq.Push(job1)

	if pq.Len() != 1 {
		t.Fatalf("expected len=1, got %d", pq.Len())
	}

	// Push same agent+subsystem with different NextRunAt
	job2 := &SubsystemJob{
		AgentID:         agentID,
		Subsystem:       "updates",
		IntervalMinutes: 30,
		NextRunAt:       now.Add(30 * time.Minute),
	}
	pq.Push(job2)

	// Should still be 1 job (updated, not added)
	if pq.Len() != 1 {
		t.Fatalf("expected len=1 after update, got %d", pq.Len())
	}

	// Verify the job was updated
	job := pq.Pop()
	if job.IntervalMinutes != 30 {
		t.Fatalf("expected interval=30, got %d", job.IntervalMinutes)
	}
	if !job.NextRunAt.Equal(now.Add(30 * time.Minute)) {
		t.Fatal("NextRunAt was not updated")
	}
}

func TestPriorityQueue_Remove(t *testing.T) {
	pq := NewPriorityQueue()

	agent1 := uuid.New()
	agent2 := uuid.New()

	pq.Push(&SubsystemJob{
		AgentID:   agent1,
		Subsystem: "updates",
		NextRunAt: time.Now(),
	})
	pq.Push(&SubsystemJob{
		AgentID:   agent2,
		Subsystem: "storage",
		NextRunAt: time.Now(),
	})

	if pq.Len() != 2 {
		t.Fatalf("expected len=2, got %d", pq.Len())
	}

	// Remove existing job
	removed := pq.Remove(agent1, "updates")
	if !removed {
		t.Fatal("Remove should return true for existing job")
	}
	if pq.Len() != 1 {
		t.Fatalf("expected len=1 after remove, got %d", pq.Len())
	}

	// Remove non-existent job
	removed = pq.Remove(agent1, "updates")
	if removed {
		t.Fatal("Remove should return false for non-existent job")
	}
	if pq.Len() != 1 {
		t.Fatal("Remove of non-existent job should not affect queue")
	}
}

func TestPriorityQueue_Get(t *testing.T) {
	pq := NewPriorityQueue()

	agentID := uuid.New()
	job := &SubsystemJob{
		AgentID:   agentID,
		Subsystem: "updates",
		NextRunAt: time.Now(),
	}
	pq.Push(job)

	// Get existing job
	retrieved := pq.Get(agentID, "updates")
	if retrieved == nil {
		t.Fatal("Get should return job")
	}
	if retrieved.AgentID != agentID {
		t.Fatal("Get returned wrong job")
	}
	if pq.Len() != 1 {
		t.Fatal("Get should not remove job")
	}

	// Get non-existent job
	retrieved = pq.Get(uuid.New(), "storage")
	if retrieved != nil {
		t.Fatal("Get should return nil for non-existent job")
	}
}

func TestPriorityQueue_PopBefore(t *testing.T) {
	pq := NewPriorityQueue()
	now := time.Now()

	// Add jobs with different NextRunAt times
	for i := 0; i < 5; i++ {
		pq.Push(&SubsystemJob{
			AgentID:   uuid.New(),
			Subsystem: "updates",
			NextRunAt: now.Add(time.Duration(i*10) * time.Minute),
		})
	}

	if pq.Len() != 5 {
		t.Fatalf("expected len=5, got %d", pq.Len())
	}

	// Pop jobs before now+25min (should get 3 jobs: 0, 10, 20 minutes)
	cutoff := now.Add(25 * time.Minute)
	jobs := pq.PopBefore(cutoff, 0) // no limit

	if len(jobs) != 3 {
		t.Fatalf("expected 3 jobs, got %d", len(jobs))
	}

	// Verify all returned jobs are before cutoff
	for _, job := range jobs {
		if job.NextRunAt.After(cutoff) {
			t.Fatalf("job NextRunAt %v is after cutoff %v", job.NextRunAt, cutoff)
		}
	}

	// Queue should have 2 jobs left
	if pq.Len() != 2 {
		t.Fatalf("expected len=2 after PopBefore, got %d", pq.Len())
	}
}

func TestPriorityQueue_PopBeforeWithLimit(t *testing.T) {
	pq := NewPriorityQueue()
	now := time.Now()

	// Add 5 jobs all due now
	for i := 0; i < 5; i++ {
		pq.Push(&SubsystemJob{
			AgentID:   uuid.New(),
			Subsystem: "updates",
			NextRunAt: now,
		})
	}

	// Pop with limit of 3
	jobs := pq.PopBefore(now.Add(1*time.Hour), 3)

	if len(jobs) != 3 {
		t.Fatalf("expected 3 jobs (limit), got %d", len(jobs))
	}

	if pq.Len() != 2 {
		t.Fatalf("expected 2 jobs remaining, got %d", pq.Len())
	}
}

func TestPriorityQueue_PeekBefore(t *testing.T) {
	pq := NewPriorityQueue()
	now := time.Now()

	pq.Push(&SubsystemJob{
		AgentID:   uuid.New(),
		Subsystem: "updates",
		NextRunAt: now.Add(5 * time.Minute),
	})
	pq.Push(&SubsystemJob{
		AgentID:   uuid.New(),
		Subsystem: "storage",
		NextRunAt: now.Add(15 * time.Minute),
	})

	// Peek before 10 minutes (should see 1 job)
	jobs := pq.PeekBefore(now.Add(10*time.Minute), 0)

	if len(jobs) != 1 {
		t.Fatalf("expected 1 job, got %d", len(jobs))
	}

	// Queue should still have both jobs
	if pq.Len() != 2 {
		t.Fatalf("expected len=2 after PeekBefore, got %d", pq.Len())
	}
}

func TestPriorityQueue_Clear(t *testing.T) {
	pq := NewPriorityQueue()

	// Add some jobs
	for i := 0; i < 10; i++ {
		pq.Push(&SubsystemJob{
			AgentID:   uuid.New(),
			Subsystem: "updates",
			NextRunAt: time.Now(),
		})
	}

	if pq.Len() != 10 {
		t.Fatalf("expected len=10, got %d", pq.Len())
	}

	// Clear the queue
	pq.Clear()

	if pq.Len() != 0 {
		t.Fatalf("expected len=0 after clear, got %d", pq.Len())
	}

	if pq.Peek() != nil {
		t.Fatal("Peek should return nil after clear")
	}
}

func TestPriorityQueue_GetStats(t *testing.T) {
	pq := NewPriorityQueue()
	now := time.Now()

	// Empty queue stats
	stats := pq.GetStats()
	if stats.Size != 0 {
		t.Fatalf("expected size=0, got %d", stats.Size)
	}
	if stats.NextRunAt != nil {
		t.Fatal("empty queue should have nil NextRunAt")
	}

	// Add jobs
	pq.Push(&SubsystemJob{
		AgentID:         uuid.New(),
		AgentHostname:   "agent-01",
		Subsystem:       "updates",
		NextRunAt:       now.Add(5 * time.Minute),
		IntervalMinutes: 15,
	})
	pq.Push(&SubsystemJob{
		AgentID:   uuid.New(),
		Subsystem: "storage",
		NextRunAt: now.Add(10 * time.Minute),
	})
	pq.Push(&SubsystemJob{
		AgentID:   uuid.New(),
		Subsystem: "updates",
		NextRunAt: now.Add(15 * time.Minute),
	})

	stats = pq.GetStats()

	if stats.Size != 3 {
		t.Fatalf("expected size=3, got %d", stats.Size)
	}

	if stats.NextRunAt == nil {
		t.Fatal("NextRunAt should not be nil")
	}

	// Should be the earliest job (5 minutes)
	expectedNext := now.Add(5 * time.Minute)
	if !stats.NextRunAt.Equal(expectedNext) {
		t.Fatalf("expected NextRunAt=%v, got %v", expectedNext, stats.NextRunAt)
	}

	// Check subsystem counts
	if stats.JobsBySubsystem["updates"] != 2 {
		t.Fatalf("expected 2 updates jobs, got %d", stats.JobsBySubsystem["updates"])
	}
	if stats.JobsBySubsystem["storage"] != 1 {
		t.Fatalf("expected 1 storage job, got %d", stats.JobsBySubsystem["storage"])
	}
}

func TestPriorityQueue_Concurrency(t *testing.T) {
	pq := NewPriorityQueue()
	var wg sync.WaitGroup

	// Concurrent pushes
	numGoroutines := 100
	wg.Add(numGoroutines)

	for i := 0; i < numGoroutines; i++ {
		go func(idx int) {
			defer wg.Done()
			pq.Push(&SubsystemJob{
				AgentID:   uuid.New(),
				Subsystem: "updates",
				NextRunAt: time.Now().Add(time.Duration(idx) * time.Second),
			})
		}(i)
	}

	wg.Wait()

	if pq.Len() != numGoroutines {
		t.Fatalf("expected len=%d after concurrent pushes, got %d", numGoroutines, pq.Len())
	}

	// Concurrent pops
	wg.Add(numGoroutines)
	popped := make(chan *SubsystemJob, numGoroutines)

	for i := 0; i < numGoroutines; i++ {
		go func() {
			defer wg.Done()
			if job := pq.Pop(); job != nil {
				popped <- job
			}
		}()
	}

	wg.Wait()
	close(popped)

	// Count popped jobs
	count := 0
	for range popped {
		count++
	}

	if count != numGoroutines {
		t.Fatalf("expected %d popped jobs, got %d", numGoroutines, count)
	}

	if pq.Len() != 0 {
		t.Fatalf("expected empty queue after concurrent pops, got len=%d", pq.Len())
	}
}

func TestPriorityQueue_ConcurrentReadWrite(t *testing.T) {
	pq := NewPriorityQueue()
	done := make(chan bool)

	// Writer goroutine
	go func() {
		for i := 0; i < 1000; i++ {
			pq.Push(&SubsystemJob{
				AgentID:   uuid.New(),
				Subsystem: "updates",
				NextRunAt: time.Now(),
			})
			time.Sleep(1 * time.Microsecond)
		}
		done <- true
	}()

	// Reader goroutine
	go func() {
		for i := 0; i < 1000; i++ {
			pq.Peek()
			pq.GetStats()
			time.Sleep(1 * time.Microsecond)
		}
		done <- true
	}()

	// Wait for both to complete
	<-done
	<-done

	// Should not panic and queue should be consistent
	if pq.Len() < 0 {
		t.Fatal("queue length became negative (race condition)")
	}
}

func BenchmarkPriorityQueue_Push(b *testing.B) {
	pq := NewPriorityQueue()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		pq.Push(&SubsystemJob{
			AgentID:   uuid.New(),
			Subsystem: "updates",
			NextRunAt: time.Now().Add(time.Duration(i) * time.Second),
		})
	}
}

func BenchmarkPriorityQueue_Pop(b *testing.B) {
	pq := NewPriorityQueue()

	// Pre-fill the queue
	for i := 0; i < b.N; i++ {
		pq.Push(&SubsystemJob{
			AgentID:   uuid.New(),
			Subsystem: "updates",
			NextRunAt: time.Now().Add(time.Duration(i) * time.Second),
		})
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		pq.Pop()
	}
}

func BenchmarkPriorityQueue_Peek(b *testing.B) {
	pq := NewPriorityQueue()

	// Pre-fill with 10000 jobs
	for i := 0; i < 10000; i++ {
		pq.Push(&SubsystemJob{
			AgentID:   uuid.New(),
			Subsystem: "updates",
			NextRunAt: time.Now().Add(time.Duration(i) * time.Second),
		})
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		pq.Peek()
	}
}
