package wal

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/dailz/go-kv/config"
	"github.com/dailz/go-kv/manifest"
)

// tinyWalConfig returns a config with a very small MaxSegmentSize to force
// quick rotation. The minimum is derived from config.Validate: we need enough
// room for the file header, batch header, physical record overhead, and block
// padding. We use 256 bytes which is well above the minimum for default
// block/batch settings.
func tinyWalConfig() *config.WalConfig {
	cfg := config.Defaults()
	// Use a small segment size to force rotation quickly.
	// MaxSegmentSize must be > WalFileHeaderSize (32) and pass Validate().
	// With defaults, minimum is around 4MB+overhead, so we must also reduce
	// MaxBatchSize and BlockSize to make a small segment valid.
	cfg.BlockSize = 512
	cfg.MaxBatchSize = 64   // very small batches
	cfg.MaxBatchEntries = 5
	cfg.MaxKeyBytes = 16
	cfg.MaxInlineValue = 16
	cfg.MaxSegmentSize = 512 // small enough to trigger rotation with a few writes
	return &cfg
}

func TestSegmentManagerCreation(t *testing.T) {
	dir := t.TempDir()
	cfg := testWalConfig()

	sm, err := NewSegmentManager(dir, 0, 1, cfg)
	if err != nil {
		t.Fatalf("NewSegmentManager: %v", err)
	}
	defer sm.Close()

	// Verify segment-0.wal exists.
	expected := filepath.Join(dir, "segment-0.wal")
	if _, err := os.Stat(expected); err != nil {
		t.Errorf("segment file %q should exist: %v", expected, err)
	}

	if sm.ActiveSegmentID() != 0 {
		t.Errorf("ActiveSegmentID = %d, want 0", sm.ActiveSegmentID())
	}

	// Verify CURRENT file points to segment-0.
	segID, ok := manifest.ReadCurrent(dir)
	if !ok {
		t.Fatal("ReadCurrent: expected CURRENT file to exist")
	}
	if segID != 0 {
		t.Errorf("CURRENT segment ID = %d, want 0", segID)
	}
}

func TestSegmentManagerRotation(t *testing.T) {
	dir := t.TempDir()
	cfg := tinyWalConfig()

	sm, err := NewSegmentManager(dir, 0, 1, cfg)
	if err != nil {
		t.Fatalf("NewSegmentManager: %v", err)
	}
	defer sm.Close()

	// Write small batches until rotation occurs.
	// Each batch is a minimal encoded WAL batch: just a small payload.
	// We'll write enough to exhaust the tiny segment.
	batch := make([]byte, 32) // 32-byte dummy batch
	for i := range batch {
		batch[i] = byte(i)
	}

	// Write until we rotate past segment 0.
	for i := 0; i < 20; i++ {
		if err := sm.AppendBatch(batch); err != nil {
			t.Fatalf("AppendBatch %d: %v", i, err)
		}
	}

	// After many writes, we should have rotated to a higher segment.
	if sm.ActiveSegmentID() == 0 {
		t.Error("expected segment rotation, but still on segment 0")
	}

	// Verify that segment-1.wal (or higher) exists on disk.
	segment1Path := filepath.Join(dir, "segment-1.wal")
	if _, err := os.Stat(segment1Path); err != nil {
		t.Errorf("segment-1.wal should exist after rotation: %v", err)
	}
}

func TestSegmentManagerBatchNotSplit(t *testing.T) {
	dir := t.TempDir()
	cfg := tinyWalConfig()

	sm, err := NewSegmentManager(dir, 0, 1, cfg)
	if err != nil {
		t.Fatalf("NewSegmentManager: %v", err)
	}
	defer sm.Close()

	// Fill segment 0 until it's nearly full.
	smallBatch := make([]byte, 16)
	for i := range smallBatch {
		smallBatch[i] = byte(i)
	}

	// Write until we're close to rotation threshold.
	for sm.RemainingPayload() > 256 {
		if err := sm.AppendBatch(smallBatch); err != nil {
			t.Fatalf("AppendBatch small: %v", err)
		}
	}

	// Now write a batch that triggers rotation.
	// This batch must go entirely into the new segment.
	triggerBatch := make([]byte, 128)
	for i := range triggerBatch {
		triggerBatch[i] = 0xAA
	}

	segIDBefore := sm.ActiveSegmentID()
	if err := sm.AppendBatch(triggerBatch); err != nil {
		t.Fatalf("AppendBatch trigger: %v", err)
	}
	segIDAfter := sm.ActiveSegmentID()

	// The trigger batch should have caused rotation (or the segment was big enough).
	// If rotation happened, verify the batch is in the new segment.
	if segIDAfter != segIDBefore {
		// Rotation occurred — the batch should be in the new segment.
		// Read the new segment file and verify it contains our trigger data.
		newSegPath := filepath.Join(dir, fmtSegName(segIDAfter))
		data, err := os.ReadFile(newSegPath)
		if err != nil {
			t.Fatalf("read new segment: %v", err)
		}
		// The trigger batch bytes should appear somewhere after the file header.
		found := false
		for i := WalFileHeaderSize; i <= len(data)-len(triggerBatch); i++ {
			if data[i] == 0xAA {
				found = true
				break
			}
		}
		if !found {
			t.Error("trigger batch data not found in new segment after rotation")
		}
	}
}

func TestSegmentManagerCurrentFile(t *testing.T) {
	dir := t.TempDir()
	cfg := tinyWalConfig()

	sm, err := NewSegmentManager(dir, 0, 1, cfg)
	if err != nil {
		t.Fatalf("NewSegmentManager: %v", err)
	}
	defer sm.Close()

	// Initial CURRENT should point to segment 0.
	segID, ok := manifest.ReadCurrent(dir)
	if !ok || segID != 0 {
		t.Fatalf("initial CURRENT: got segment %d, ok=%v, want 0", segID, ok)
	}

	// Write enough to force rotation.
	batch := make([]byte, 32)
	for i := 0; i < 20; i++ {
		if err := sm.AppendBatch(batch); err != nil {
			t.Fatalf("AppendBatch %d: %v", i, err)
		}
	}

	// CURRENT should now point to the active segment.
	currentSegID, ok := manifest.ReadCurrent(dir)
	if !ok {
		t.Fatal("ReadCurrent after rotation: expected CURRENT file to exist")
	}
	if currentSegID != sm.ActiveSegmentID() {
		t.Errorf("CURRENT segment ID = %d, want %d", currentSegID, sm.ActiveSegmentID())
	}
}

func TestSegmentManagerSync(t *testing.T) {
	dir := t.TempDir()
	cfg := testWalConfig()

	sm, err := NewSegmentManager(dir, 0, 1, cfg)
	if err != nil {
		t.Fatalf("NewSegmentManager: %v", err)
	}
	defer sm.Close()

	if err := sm.Sync(); err != nil {
		t.Errorf("Sync: %v", err)
	}
}

func TestSegmentManagerRemainingPayload(t *testing.T) {
	dir := t.TempDir()
	cfg := testWalConfig()

	sm, err := NewSegmentManager(dir, 0, 1, cfg)
	if err != nil {
		t.Fatalf("NewSegmentManager: %v", err)
	}
	defer sm.Close()

	expected := cfg.MaxSegmentSize - WalFileHeaderSize
	if got := sm.RemainingPayload(); got != expected {
		t.Errorf("RemainingPayload = %d, want %d", got, expected)
	}
}

// fmtSegName formats a segment filename.
func fmtSegName(segID uint64) string {
	return filepath.Join("", "segment-"+itoa(segID)+".wal")
}

func itoa(n uint64) string {
	if n == 0 {
		return "0"
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	return string(buf[i:])
}
