package server

import (
	"fmt"
	"testing"
)

func TestJumpConsistentHash(t *testing.T) {
	t.Parallel()
	// Same key + same bucket count → same bucket.
	for _, key := range []uint64{0, 1, 42, 1000, 1 << 33, ^uint64(0)} {
		a := jumpConsistentHash(key, 10)
		b := jumpConsistentHash(key, 10)
		if a != b {
			t.Fatalf("jumpConsistentHash not deterministic: key=%d got %d then %d", key, a, b)
		}
		if a < 0 || a >= 10 {
			t.Fatalf("jumpConsistentHash out of range: key=%d bucket=%d", key, a)
		}
	}
}

func TestJumpConsistentHashDistribution(t *testing.T) {
	t.Parallel()
	const numBuckets = 10
	const numKeys = 100000
	counts := make([]int, numBuckets)
	for i := 0; i < numKeys; i++ {
		bucket := jumpConsistentHash(tenantHashKey(tenantIDForIndex(i)), numBuckets)
		if bucket < 0 || bucket >= numBuckets {
			t.Fatalf("bucket out of range: %d", bucket)
		}
		counts[bucket]++
	}
	// Each bucket should get ~10% of keys. Allow ±50% tolerance for this small test.
	expected := numKeys / numBuckets
	for i, c := range counts {
		if c < expected/2 || c > expected*2 {
			t.Fatalf("bucket %d count %d outside expected range %d±50%%", i, c, expected)
		}
	}
}

func TestTenantHashKeyDeterministic(t *testing.T) {
	t.Parallel()
	a := tenantHashKey("tenant-abc")
	b := tenantHashKey("tenant-abc")
	if a != b {
		t.Fatal("tenantHashKey not deterministic")
	}
	c := tenantHashKey("tenant-def")
	if a == c {
		t.Fatal("tenantHashKey collision for different inputs")
	}
}

func TestShardResolverSinglePod(t *testing.T) {
	t.Parallel()
	// No meta store → single-pod mode → owns everything.
	r := newSemanticShardResolver(nil, "pod1", 0)
	if !r.ownsTenant("tenant1") {
		t.Fatal("single-pod resolver should own all tenants")
	}
	if !r.ownsTenant("") {
		t.Fatal("single-pod resolver should own even empty tenant")
	}
}

func TestShardResolverOwnsTenantFn(t *testing.T) {
	t.Parallel()
	r := newSemanticShardResolver(nil, "", 0)
	fn := r.ownsTenantFn()
	if fn == nil {
		t.Fatal("ownsTenantFn should not be nil")
	}
	if !fn("any-tenant") {
		t.Fatal("single-pod ownsTenantFn should own everything")
	}
}

func tenantIDForIndex(i int) string {
	return "tenant-" + string(rune('A'+i%26)) + string(rune('A'+(i/26)%26))
}

func TestShardResolverOwnershipFollowsRing(t *testing.T) {
	t.Parallel()
	r := newSemanticShardResolver(nil, "pod-a", 0)
	if !r.ownsTenant("any") {
		t.Fatal("empty ring should own every tenant")
	}
	r.applyRing([]string{"pod-a", "pod-b"})
	var owned, other string
	for i := 0; i < 1000; i++ {
		id := fmt.Sprintf("tenant-ring-%d", i)
		if r.ownsTenant(id) {
			if owned == "" {
				owned = id
			}
		} else if other == "" {
			other = id
		}
		if owned != "" && other != "" {
			break
		}
	}
	if owned == "" || other == "" {
		t.Fatal("2-pod ring should split tenants")
	}
	r.applyRing([]string{"pod-a"})
	if !r.ownsTenant(other) {
		t.Fatal("single-pod ring should own previously unowned tenant")
	}
}

func TestShardResolverApplyRingFiresOnlyOnChange(t *testing.T) {
	t.Parallel()
	r := newSemanticShardResolver(nil, "pod-a", 0)
	calls := 0
	r.setOnRingRefresh(func() { calls++ })
	r.applyRing([]string{"pod-a", "pod-b"})
	if calls != 1 {
		t.Fatalf("first ring store calls=%d, want 1", calls)
	}
	r.applyRing([]string{"pod-b", "pod-a"}) // same set, sort-normalized
	if calls != 1 {
		t.Fatalf("identical ring calls=%d, want 1", calls)
	}
	r.applyRing([]string{"pod-a", "pod-b", "pod-c"})
	if calls != 2 {
		t.Fatalf("expanded ring calls=%d, want 2", calls)
	}
}
