package orchestrator

import (
	"context"
	"sync"
	"testing"
	"time"
)

func TestMemoryRunQueue_DepthReportsPending(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(8, time.Second)
	for i := 0; i < 3; i++ {
		_ = q.Enqueue(context.Background(), QueueJob{TaskID: "t", RunID: string(rune('a' + i))})
	}
	depth, err := q.Depth(context.Background())
	if err != nil {
		t.Fatalf("Depth: %v", err)
	}
	if depth != 3 {
		t.Fatalf("depth = %d, want 3", depth)
	}
}

func TestMemoryRunQueue_CapacityRejectsWhenFull(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(2, time.Second)
	for i := 0; i < 2; i++ {
		if err := q.Enqueue(context.Background(), QueueJob{RunID: string(rune('a' + i))}); err != nil {
			t.Fatalf("enqueue %d: %v", i, err)
		}
	}
	// Third enqueue should fail or block; we use a context with timeout to detect.
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	err := q.Enqueue(ctx, QueueJob{RunID: "overflow"})
	if err == nil {
		t.Skip("queue accepted overflow — capacity may be advisory")
	}
}

func TestMemoryRunQueue_ExtendLeaseKeepsClaim(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(8, 30*time.Millisecond)
	_ = q.Enqueue(context.Background(), QueueJob{RunID: "extend"})

	claim, ok, _ := q.Claim(context.Background(), "worker_a", 100*time.Millisecond)
	if !ok {
		t.Fatal("first claim failed")
	}

	// Extend before original lease expires.
	time.Sleep(15 * time.Millisecond)
	if err := q.ExtendLease(context.Background(), claim.ClaimID, 100*time.Millisecond); err != nil {
		t.Fatalf("ExtendLease: %v", err)
	}
	// Wait past the original lease — extension should hold.
	time.Sleep(30 * time.Millisecond)

	_, ok2, _ := q.Claim(context.Background(), "worker_b", 50*time.Millisecond)
	if ok2 {
		t.Fatal("worker_b should not be able to claim — lease was extended")
	}
}

func TestMemoryRunQueue_ExpiredLeaseCanBeReclaimed(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(8, 20*time.Millisecond)
	if err := q.Enqueue(context.Background(), QueueJob{TaskID: "task", RunID: "reclaim"}); err != nil {
		t.Fatalf("Enqueue: %v", err)
	}

	claim, ok, err := q.Claim(context.Background(), "worker_a", 50*time.Millisecond)
	if err != nil || !ok {
		t.Fatalf("first Claim: ok=%v err=%v", ok, err)
	}
	if claim.Job.RunID != "reclaim" {
		t.Fatalf("first claim run id = %q, want reclaim", claim.Job.RunID)
	}

	time.Sleep(35 * time.Millisecond)

	reclaimed, ok, err := q.Claim(context.Background(), "worker_b", 50*time.Millisecond)
	if err != nil || !ok {
		t.Fatalf("reclaim Claim: ok=%v err=%v", ok, err)
	}
	if reclaimed.Job.RunID != "reclaim" {
		t.Fatalf("reclaimed run id = %q, want reclaim", reclaimed.Job.RunID)
	}
	if reclaimed.ClaimID == claim.ClaimID {
		t.Fatalf("reclaimed claim id = %q, want a fresh claim id", reclaimed.ClaimID)
	}
}

func TestMemoryRunQueue_ConcurrentEnqueueClaim(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(100, time.Second)

	var wg sync.WaitGroup
	const n = 50
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			_ = q.Enqueue(context.Background(), QueueJob{RunID: string(rune('a' + id%26))})
		}(i)
	}
	wg.Wait()

	depth, _ := q.Depth(context.Background())
	if depth != n {
		t.Fatalf("after concurrent enqueue depth = %d, want %d", depth, n)
	}

	// Drain via concurrent claims.
	claimed := make(chan string, n)
	for i := 0; i < n; i++ {
		go func() {
			c, ok, _ := q.Claim(context.Background(), "w", 100*time.Millisecond)
			if ok {
				claimed <- c.ClaimID
			} else {
				claimed <- ""
			}
		}()
	}

	got := 0
	for i := 0; i < n; i++ {
		if id := <-claimed; id != "" {
			got++
		}
	}
	if got < n/2 {
		t.Fatalf("only claimed %d/%d concurrently — likely a contention bug", got, n)
	}
}

func TestMemoryRunQueue_ClaimReturnsFalseWhenEmpty(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(8, time.Second)
	_, ok, err := q.Claim(context.Background(), "w", 10*time.Millisecond)
	if err != nil {
		t.Fatalf("Claim: %v", err)
	}
	if ok {
		t.Fatal("ok = true on empty queue")
	}
}

func TestMemoryRunQueue_AckOnUnknownClaimIsNoop(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(8, time.Second)
	// Memory queue's Ack on unknown claim is a no-op (idempotent).
	if err := q.Ack(context.Background(), "nonexistent-claim"); err != nil {
		t.Fatalf("Ack: %v", err)
	}
}

func TestMemoryRunQueue_NackOnUnknownClaimIsNoop(t *testing.T) {
	t.Parallel()
	q := NewMemoryRunQueue(8, time.Second)
	if err := q.Nack(context.Background(), "nonexistent", "reason"); err != nil {
		t.Fatalf("Nack: %v", err)
	}
}
