package wal

import (
	"errors"
	"math"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/dailz/go-kv/errkit"
)

func TestSequenceAllocation(t *testing.T) {
	sm := NewSequenceManager(0)

	base, err := sm.AllocateBatch(5)
	if err != nil {
		t.Fatalf("AllocateBatch(5): %v", err)
	}
	if base != 0 {
		t.Fatalf("expected base=0, got %d", base)
	}

	base, err = sm.AllocateBatch(3)
	if err != nil {
		t.Fatalf("AllocateBatch(3): %v", err)
	}
	if base != 5 {
		t.Fatalf("expected base=5, got %d", base)
	}

	base, err = sm.AllocateBatch(1)
	if err != nil {
		t.Fatalf("AllocateBatch(1): %v", err)
	}
	if base != 8 {
		t.Fatalf("expected base=8, got %d", base)
	}

	if sm.NextSequence() != 9 {
		t.Fatalf("expected NextSequence=9, got %d", sm.NextSequence())
	}
}

func TestSequenceOverflow(t *testing.T) {
	nearMax := uint64(math.MaxUint64 - 2)
	sm := NewSequenceManager(nearMax)

	// Remaining: MaxUint64-2, MaxUint64-1, MaxUint64 = 3 slots.
	// Asking for 5 should overflow.
	_, err := sm.AllocateBatch(5)
	if !errors.Is(err, errkit.ErrSequenceExhausted) {
		t.Fatalf("expected ErrSequenceExhausted, got %v", err)
	}

	// 3 should still succeed.
	base, err := sm.AllocateBatch(3)
	if err != nil {
		t.Fatalf("AllocateBatch(3): %v", err)
	}
	if base != nearMax {
		t.Fatalf("expected base=%d, got %d", nearMax, base)
	}

	// Now any further allocation should fail.
	_, err = sm.AllocateBatch(1)
	if !errors.Is(err, errkit.ErrSequenceExhausted) {
		t.Fatalf("expected ErrSequenceExhausted after exhaustion, got %v", err)
	}
}

func TestPublishAdvance(t *testing.T) {
	sm := NewSequenceManager(0)

	sm.Publish(10)
	if sm.Published() != 10 {
		t.Fatalf("expected Published=10, got %d", sm.Published())
	}

	// Publishing a lower value must not decrease the watermark.
	sm.Publish(5)
	if sm.Published() != 10 {
		t.Fatalf("expected Published=10 (no decrease), got %d", sm.Published())
	}

	sm.Publish(15)
	if sm.Published() != 15 {
		t.Fatalf("expected Published=15, got %d", sm.Published())
	}
}

func TestMarkDurable(t *testing.T) {
	sm := NewSequenceManager(0)

	sm.MarkDurable(8)
	if sm.Durable() != 8 {
		t.Fatalf("expected Durable=8, got %d", sm.Durable())
	}

	// Lower value must not decrease.
	sm.MarkDurable(3)
	if sm.Durable() != 8 {
		t.Fatalf("expected Durable=8 (no decrease), got %d", sm.Durable())
	}
}

func TestZeroCountRejected(t *testing.T) {
	sm := NewSequenceManager(0)

	_, err := sm.AllocateBatch(0)
	if !errors.Is(err, errkit.ErrSequenceExhausted) {
		t.Fatalf("expected ErrSequenceExhausted for count=0, got %v", err)
	}
}

func TestConcurrentAllocation(t *testing.T) {
	const goroutines = 16
	const batchSize uint32 = 100

	sm := NewSequenceManager(0)

	var totalAllocated atomic.Uint64
	var wg sync.WaitGroup
	wg.Add(goroutines)

	for i := 0; i < goroutines; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 50; j++ {
				base, err := sm.AllocateBatch(batchSize)
				if err != nil {
					t.Errorf("AllocateBatch failed: %v", err)
					return
				}
				totalAllocated.Add(uint64(batchSize))

				// Verify no overlap: base must be aligned to batchSize increments
				// and within valid range. The key property is no gaps.
				_ = base
			}
		}()
	}

	wg.Wait()

	expected := uint64(goroutines) * 50 * uint64(batchSize)
	if totalAllocated.Load() != expected {
		t.Fatalf("expected total allocated=%d, got %d", expected, totalAllocated.Load())
	}

	if sm.NextSequence() != expected {
		t.Fatalf("expected NextSequence=%d, got %d", expected, sm.NextSequence())
	}
}
