package page_writer

import (
	"sync/atomic"
	"testing"
	"time"
)

func TestWriteBufferAccountant_Unlimited(t *testing.T) {
	a := NewWriteBufferAccountant(0)
	a.Reserve(1 << 30)
	a.Reserve(1 << 30)
	if got := a.Used(); got != 0 {
		t.Fatalf("unlimited accountant should not track usage, got %d", got)
	}
}

func TestWriteBufferAccountant_Nil(t *testing.T) {
	var a *WriteBufferAccountant
	a.Reserve(100) // must not panic
	a.Release(100)
	if a.Used() != 0 {
		t.Fatal("nil Used should return 0")
	}
}

func TestWriteBufferAccountant_ReserveAndRelease(t *testing.T) {
	a := NewWriteBufferAccountant(300)
	a.Reserve(100)
	a.Reserve(100)
	if got := a.Used(); got != 200 {
		t.Fatalf("expected 200 used, got %d", got)
	}
	a.Release(100)
	if got := a.Used(); got != 100 {
		t.Fatalf("expected 100 used after release, got %d", got)
	}
}

func TestWriteBufferAccountant_BlocksWhenOverCap(t *testing.T) {
	a := NewWriteBufferAccountant(100)
	a.Reserve(100)

	started := make(chan struct{})
	var landed atomic.Bool
	go func() {
		close(started)
		a.Reserve(50) // should block — used=100, cap=100
		landed.Store(true)
	}()

	// Wait until the goroutine is about to enter Reserve. Once it does,
	// Reserve cannot make progress until Release is called (cap is
	// full), so landed is guaranteed to stay false until we release.
	<-started
	if landed.Load() {
		t.Fatal("second reserve should be blocked while cap is full")
	}

	a.Release(100)

	deadline := time.Now().Add(time.Second)
	for !landed.Load() && time.Now().Before(deadline) {
		time.Sleep(5 * time.Millisecond)
	}
	if !landed.Load() {
		t.Fatal("second reserve should unblock after release")
	}
	if got := a.Used(); got != 50 {
		t.Fatalf("expected 50 used after handoff, got %d", got)
	}
}

func TestWriteBufferAccountant_SoftThrottle(t *testing.T) {
	// Cap = 1000, soft threshold = 800. Reserve 850 (85%) then measure
	// that the next Reserve is delayed by at least softThrottleDelay.
	a := NewWriteBufferAccountant(1000)
	a.Reserve(850)

	start := time.Now()
	a.Reserve(10) // should trigger soft throttle (used=850 > 800)
	elapsed := time.Since(start)

	if elapsed < softThrottleDelay {
		t.Fatalf("expected Reserve to take >= %v (soft throttle), took %v", softThrottleDelay, elapsed)
	}
}

func TestWriteBufferAccountant_HardThrottle(t *testing.T) {
	// Cap = 1000, hard threshold = 950. Reserve 960 (96%) then measure
	// that the next Reserve is delayed by at least hardThrottleDelay.
	a := NewWriteBufferAccountant(1000)
	a.Reserve(960)

	start := time.Now()
	a.Reserve(10) // should trigger hard throttle (used=960 > 950)
	elapsed := time.Since(start)

	if elapsed < hardThrottleDelay {
		t.Fatalf("expected Reserve to take >= %v (hard throttle), took %v", hardThrottleDelay, elapsed)
	}
}

func TestWriteBufferAccountant_NoThrottleBelowSoft(t *testing.T) {
	// Cap = 1000, soft threshold = 800. Reserve 700 (70%) -- below soft
	// threshold, so subsequent Reserve should return quickly.
	a := NewWriteBufferAccountant(1000)
	a.Reserve(700)

	start := time.Now()
	a.Reserve(10)
	elapsed := time.Since(start)

	if elapsed >= softThrottleDelay {
		t.Fatalf("expected Reserve below soft threshold to avoid throttle delay (< %v), took %v", softThrottleDelay, elapsed)
	}
}

func TestWriteBufferAccountant_ThrottleCounters(t *testing.T) {
	a := NewWriteBufferAccountant(1000)

	// Below soft -- no throttle
	a.Reserve(700)
	a.Reserve(10)
	if a.SoftThrottleCount() != 0 || a.HardThrottleCount() != 0 {
		t.Fatalf("expected no throttle counts below soft threshold, got soft=%d hard=%d",
			a.SoftThrottleCount(), a.HardThrottleCount())
	}

	// Release back down, then fill to soft zone
	a.Release(710)
	a.Reserve(850) // now used=850, above soft (800)
	a.Reserve(10)  // triggers soft throttle
	if a.SoftThrottleCount() != 1 {
		t.Fatalf("expected soft throttle count 1, got %d", a.SoftThrottleCount())
	}
	if a.HardThrottleCount() != 0 {
		t.Fatalf("expected hard throttle count 0, got %d", a.HardThrottleCount())
	}

	// Release and fill to hard zone
	a.Release(860)
	a.Reserve(960) // now used=960, above hard (950)
	a.Reserve(10)  // triggers hard throttle
	if a.HardThrottleCount() != 1 {
		t.Fatalf("expected hard throttle count 1, got %d", a.HardThrottleCount())
	}
}

func TestWriteBufferAccountant_GraduatedRecovery(t *testing.T) {
	// Fill to hard threshold, verify hard throttle. Release to soft zone,
	// verify that throttle level decreases to soft.
	a := NewWriteBufferAccountant(1000)

	// Fill to hard zone (960 > 950)
	a.Reserve(960)
	start := time.Now()
	a.Reserve(10) // hard throttle
	elapsed := time.Since(start)
	if elapsed < hardThrottleDelay {
		t.Fatalf("expected hard throttle delay, got %v", elapsed)
	}
	if a.HardThrottleCount() != 1 {
		t.Fatalf("expected hard throttle count 1, got %d", a.HardThrottleCount())
	}

	// Release down to soft zone: used = 970 - 130 = 840 (> 800 soft, < 950 hard)
	a.Release(130)

	start = time.Now()
	a.Reserve(10) // should now be soft throttle
	elapsed = time.Since(start)
	if elapsed < softThrottleDelay {
		t.Fatalf("expected soft throttle delay after recovery, got %v", elapsed)
	}
	if a.HardThrottleCount() != 1 {
		t.Fatalf("expected hard throttle count to remain 1 after recovery, got %d", a.HardThrottleCount())
	}
	if a.SoftThrottleCount() != 1 {
		t.Fatalf("expected soft throttle count 1 after recovery, got %d", a.SoftThrottleCount())
	}
}

func TestWriteBufferAccountant_AllowsOversizeWhenEmpty(t *testing.T) {
	// A single request larger than cap must succeed when nothing is in
	// flight, otherwise a single file handle with a large chunk size would
	// deadlock forever.
	a := NewWriteBufferAccountant(100)
	done := make(chan struct{})
	go func() {
		a.Reserve(500)
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("oversize reserve on empty accountant should not block")
	}
}
