package sandbox

import (
	"fmt"
	"sort"
	"sync"
	"testing"
)

func TestCIDAllocatorLifecycle(t *testing.T) {
	allocator := NewCIDAllocator()

	first, err := allocator.AllocateCID("sandbox-a")
	if err != nil {
		t.Fatalf("AllocateCID(sandbox-a) error = %v", err)
	}
	if first != MinCID {
		t.Fatalf("AllocateCID(sandbox-a) = %d, want %d", first, MinCID)
	}

	again, err := allocator.AllocateCID("sandbox-a")
	if err != nil {
		t.Fatalf("AllocateCID(sandbox-a) again error = %v", err)
	}
	if again != first {
		t.Fatalf("AllocateCID(sandbox-a) again = %d, want %d", again, first)
	}

	second, err := allocator.AllocateCID("sandbox-b")
	if err != nil {
		t.Fatalf("AllocateCID(sandbox-b) error = %v", err)
	}
	if second != first+1 {
		t.Fatalf("AllocateCID(sandbox-b) = %d, want %d", second, first+1)
	}

	if err := allocator.ReleaseCID("sandbox-a"); err != nil {
		t.Fatalf("ReleaseCID(sandbox-a) error = %v", err)
	}
	if _, ok := allocator.GetCID("sandbox-a"); ok {
		t.Fatal("GetCID(sandbox-a) found released CID")
	}

	third, err := allocator.AllocateCID("sandbox-c")
	if err != nil {
		t.Fatalf("AllocateCID(sandbox-c) error = %v", err)
	}
	if third != second+1 {
		t.Fatalf("AllocateCID(sandbox-c) = %d, want monotonic CID %d", third, second+1)
	}

	if got := allocator.GetActiveCount(); got != 2 {
		t.Fatalf("GetActiveCount() = %d, want 2", got)
	}
	active := allocator.ListActiveSandboxes()
	sort.Strings(active)
	if len(active) != 2 || active[0] != "sandbox-b" || active[1] != "sandbox-c" {
		t.Fatalf("ListActiveSandboxes() = %v, want [sandbox-b sandbox-c]", active)
	}
}

func TestCIDAllocatorConcurrentAllocation(t *testing.T) {
	const sandboxCount = 128
	allocator := NewCIDAllocator()

	var wg sync.WaitGroup
	results := make(chan uint32, sandboxCount)
	errs := make(chan error, sandboxCount)
	for i := range sandboxCount {
		wg.Add(1)
		go func() {
			defer wg.Done()
			cid, err := allocator.AllocateCID(fmt.Sprintf("sandbox-%d", i))
			if err != nil {
				errs <- err
				return
			}
			results <- cid
		}()
	}
	wg.Wait()
	close(results)
	close(errs)

	for err := range errs {
		t.Errorf("AllocateCID() error = %v", err)
	}
	seen := make(map[uint32]struct{}, sandboxCount)
	for cid := range results {
		if _, ok := seen[cid]; ok {
			t.Errorf("CID %d allocated more than once", cid)
		}
		seen[cid] = struct{}{}
	}
	if len(seen) != sandboxCount {
		t.Fatalf("allocated %d unique CIDs, want %d", len(seen), sandboxCount)
	}
}

func TestCIDAllocatorExhaustion(t *testing.T) {
	allocator := NewCIDAllocator()
	allocator.nextCID = MaxCID

	cid, err := allocator.AllocateCID("last-sandbox")
	if err != nil {
		t.Fatalf("AllocateCID(last-sandbox) error = %v", err)
	}
	if cid != MaxCID {
		t.Fatalf("AllocateCID(last-sandbox) = %d, want %d", cid, MaxCID)
	}

	if _, err := allocator.AllocateCID("overflow-sandbox"); err == nil {
		t.Fatal("AllocateCID(overflow-sandbox) error = nil, want exhaustion error")
	}
}

func TestCIDAllocatorStateIsProcessLocal(t *testing.T) {
	first := NewCIDAllocator()
	if _, err := first.AllocateCID("sandbox-a"); err != nil {
		t.Fatalf("first AllocateCID(sandbox-a) error = %v", err)
	}

	second := NewCIDAllocator()
	cid, err := second.AllocateCID("sandbox-b")
	if err != nil {
		t.Fatalf("second AllocateCID(sandbox-b) error = %v", err)
	}
	if cid != MinCID {
		t.Fatalf("second allocator first CID = %d, want %d", cid, MinCID)
	}
}