package wal

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

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

func TestRecoverFromEmptyDir(t *testing.T) {
	dir := t.TempDir()

	replayer := &mockReplayer{}
	result, err := Recover(dir, replayer)
	if err != nil {
		t.Fatalf("Recover empty dir: %v", err)
	}
	if result.NextSequence != 0 {
		t.Errorf("NextSequence = %d, want 0", result.NextSequence)
	}
	if result.Truncated {
		t.Error("Truncated = true, want false")
	}
	if result.TruncateError != nil {
		t.Errorf("TruncateError = %v, want nil", result.TruncateError)
	}
}

func TestRecoverFullFlow(t *testing.T) {
	dir := t.TempDir()

	// Write test data: 2 batches across 1 segment.
	writeTestSegment(t, dir, 0, 0, [][]*WalEntry{
		{makePutEntry("key1", "val1"), makeDeleteEntry("key2")},
		{makePutEntry("key3", "val3")},
	})

	replayer := &mockReplayer{}
	result, err := Recover(dir, replayer)
	if err != nil {
		t.Fatalf("Recover: %v", err)
	}
	if result.NextSequence != 3 {
		t.Errorf("NextSequence = %d, want 3", result.NextSequence)
	}
	if result.NextSegmentID != 1 {
		t.Errorf("NextSegmentID = %d, want 1", result.NextSegmentID)
	}
	if result.Truncated {
		t.Error("Truncated = true, want false")
	}

	wantPuts := []replayPut{
		{key: "key1", value: "val1", seq: 0},
		{key: "key3", value: "val3", seq: 2},
	}
	if !reflect.DeepEqual(replayer.puts, wantPuts) {
		t.Errorf("puts = %#v, want %#v", replayer.puts, wantPuts)
	}
	wantDeletes := []replayDelete{
		{key: "key2", seq: 1},
	}
	if !reflect.DeepEqual(replayer.deletes, wantDeletes) {
		t.Errorf("deletes = %#v, want %#v", replayer.deletes, wantDeletes)
	}
}

func TestRecoverWithTailCorruption(t *testing.T) {
	dir := t.TempDir()

	// Write valid batches, then corrupt the tail.
	filePath := writeTestSegment(t, dir, 0, 100, [][]*WalEntry{
		{makePutEntry("good1", "before-corruption")},
		{makePutEntry("good2", "also-before")},
	})
	appendFileBytes(t, filePath, []byte{0xDE, 0xAD, 0xBE, 0xEF})

	replayer := &mockReplayer{}
	result, err := Recover(dir, replayer)
	if err != nil {
		t.Fatalf("Recover with tail corruption: %v", err)
	}
	if !result.Truncated {
		t.Fatal("Truncated = false, want true")
	}
	if result.TruncateError == nil {
		t.Fatal("TruncateError = nil, want non-nil")
	}
	if result.NextSequence != 102 {
		t.Errorf("NextSequence = %d, want 102", result.NextSequence)
	}
	if result.NextSegmentID != 1 {
		t.Errorf("NextSegmentID = %d, want 1", result.NextSegmentID)
	}

	// Verify only the valid entries were replayed.
	wantPuts := []replayPut{
		{key: "good1", value: "before-corruption", seq: 100},
		{key: "good2", value: "also-before", seq: 101},
	}
	if !reflect.DeepEqual(replayer.puts, wantPuts) {
		t.Errorf("puts = %#v, want %#v", replayer.puts, wantPuts)
	}

	// Verify the file was truncated — it should be smaller than before.
	fi, fiErr := os.Stat(filePath)
	if fiErr != nil {
		t.Fatalf("Stat truncated file: %v", fiErr)
	}
	if fi.Size() == 0 {
		t.Error("truncated file is empty")
	}

	// Verify the truncated file still parses cleanly.
	replayer2 := &mockReplayer{}
	_, parseErr := ReplaySegmentFile(filePath, 100, replayer2)
	if parseErr != nil {
		t.Fatalf("replay after truncation: %v", parseErr)
	}
	if !reflect.DeepEqual(replayer2.puts, wantPuts) {
		t.Errorf("replay after truncation puts = %#v, want %#v", replayer2.puts, wantPuts)
	}
}

func TestRecoverDoesNotUpdateManifest(t *testing.T) {
	dir := t.TempDir()

	writeTestSegment(t, dir, 0, 50, [][]*WalEntry{
		{makePutEntry("a", "b")},
		{makePutEntry("c", "d")},
	})

	beforeMF, err := manifest.Load(dir)
	if err != nil {
		t.Fatalf("manifest.Load before recover: %v", err)
	}
	beforeExists := fileExists(t, filepath.Join(dir, "MANIFEST"))

	replayer := &mockReplayer{}
	if _, err := Recover(dir, replayer); err != nil {
		t.Fatalf("Recover: %v", err)
	}

	afterMF, err := manifest.Load(dir)
	if err != nil {
		t.Fatalf("manifest.Load after recover: %v", err)
	}
	if afterMF.RecoverySegmentID != beforeMF.RecoverySegmentID {
		t.Errorf("MANIFEST RecoverySegmentID changed: %d -> %d",
			beforeMF.RecoverySegmentID, afterMF.RecoverySegmentID)
	}
	afterExists := fileExists(t, filepath.Join(dir, "MANIFEST"))
	if beforeExists != afterExists {
		t.Errorf("MANIFEST file existence changed: before=%v after=%v",
			beforeExists, afterExists)
	}
}

// Regression guard for C3: covers the case where MANIFEST already exists.
// The fresh-DB test above cannot catch accidental overwrites of an existing
// MANIFEST.
func TestRecoverPreservesExistingManifest(t *testing.T) {
	dir := t.TempDir()

	writeTestSegment(t, dir, 0, 50, [][]*WalEntry{
		{makePutEntry("a", "b")},
	})

	if err := manifest.Save(dir, 0); err != nil {
		t.Fatalf("manifest.Save setup: %v", err)
	}
	beforeBytes, err := os.ReadFile(filepath.Join(dir, "MANIFEST"))
	if err != nil {
		t.Fatalf("ReadFile MANIFEST: %v", err)
	}

	replayer := &mockReplayer{}
	if _, err := Recover(dir, replayer); err != nil {
		t.Fatalf("Recover: %v", err)
	}

	afterBytes, err := os.ReadFile(filepath.Join(dir, "MANIFEST"))
	if err != nil {
		t.Fatalf("ReadFile MANIFEST after recover: %v", err)
	}
	if !bytes.Equal(beforeBytes, afterBytes) {
		t.Errorf("MANIFEST bytes changed:\n  before=%q\n  after=%q",
			string(beforeBytes), string(afterBytes))
	}
}

// Regression guard for C3: design §3.2 line 280 requires recovery to be
// idempotent on a clean WAL (no MANIFEST side effects).
func TestRecoverIdempotentClean(t *testing.T) {
	dir := t.TempDir()

	writeTestSegment(t, dir, 0, 0, [][]*WalEntry{
		{makePutEntry("k1", "v1")},
		{makePutEntry("k2", "v2")},
	})

	replayer1 := &mockReplayer{}
	result1, err := Recover(dir, replayer1)
	if err != nil {
		t.Fatalf("Recover (1st): %v", err)
	}

	replayer2 := &mockReplayer{}
	result2, err := Recover(dir, replayer2)
	if err != nil {
		t.Fatalf("Recover (2nd): %v", err)
	}

	if result1.NextSequence != result2.NextSequence {
		t.Errorf("NextSequence differs: %d vs %d", result1.NextSequence, result2.NextSequence)
	}
	if result1.NextSegmentID != result2.NextSegmentID {
		t.Errorf("NextSegmentID differs: %d vs %d", result1.NextSegmentID, result2.NextSegmentID)
	}
	if result1.Truncated || result2.Truncated {
		t.Errorf("Truncated should be false for clean WAL: r1=%v r2=%v",
			result1.Truncated, result2.Truncated)
	}
	if !reflect.DeepEqual(replayer1.puts, replayer2.puts) {
		t.Errorf("replayed puts differ:\n  r1=%#v\n  r2=%#v", replayer1.puts, replayer2.puts)
	}
}

// Regression guard for C3: after the first recovery truncates a corrupted
// tail, the second recovery must observe stable state (Truncated=false, same
// NextSequence, same replayed entries).
func TestRecoverIdempotentAfterTruncation(t *testing.T) {
	dir := t.TempDir()

	filePath := writeTestSegment(t, dir, 0, 100, [][]*WalEntry{
		{makePutEntry("good1", "before-corruption")},
		{makePutEntry("good2", "also-before")},
	})
	appendFileBytes(t, filePath, []byte{0xDE, 0xAD, 0xBE, 0xEF})

	replayer1 := &mockReplayer{}
	result1, err := Recover(dir, replayer1)
	if err != nil {
		t.Fatalf("Recover (1st): %v", err)
	}
	if !result1.Truncated {
		t.Fatal("1st Recover: Truncated = false, want true")
	}

	replayer2 := &mockReplayer{}
	result2, err := Recover(dir, replayer2)
	if err != nil {
		t.Fatalf("Recover (2nd): %v", err)
	}
	if result2.Truncated {
		t.Error("2nd Recover: Truncated = true, want false (tail already repaired)")
	}
	if result1.NextSequence != result2.NextSequence {
		t.Errorf("NextSequence differs: %d vs %d", result1.NextSequence, result2.NextSequence)
	}
	if !reflect.DeepEqual(replayer1.puts, replayer2.puts) {
		t.Errorf("replayed puts differ:\n  r1=%#v\n  r2=%#v", replayer1.puts, replayer2.puts)
	}
}

// Regression guard for C2: design §3.2 line 604-06 forbids using CURRENT as
// recovery start. writeTestSegment uses NewSegmentWriter directly, which does
// NOT write CURRENT (only SegmentManager does), so we write CURRENT explicitly
// to simulate the Phase 1 default state.
func TestRecoverIgnoresCurrentFallback(t *testing.T) {
	dir := t.TempDir()

	writeTestSegment(t, dir, 0, 0, [][]*WalEntry{
		{makePutEntry("seg0-k1", "v1")},
	})
	writeTestSegment(t, dir, 1, 1, [][]*WalEntry{
		{makePutEntry("seg1-k1", "v1")},
	})
	writeTestSegment(t, dir, 2, 2, [][]*WalEntry{
		{makePutEntry("seg2-k1", "v1")},
	})

	if err := manifest.WriteCurrent(dir, 2); err != nil {
		t.Fatalf("WriteCurrent: %v", err)
	}

	currentSegID, ok := manifest.ReadCurrent(dir)
	if !ok || currentSegID != 2 {
		t.Fatalf("CURRENT setup wrong: segID=%d ok=%v", currentSegID, ok)
	}
	mf, err := manifest.Load(dir)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if mf.RecoverySegmentID != 0 {
		t.Fatalf("MANIFEST.RecoverySegmentID = %d, want 0", mf.RecoverySegmentID)
	}

	replayer := &mockReplayer{}
	result, err := Recover(dir, replayer)
	if err != nil {
		t.Fatalf("Recover: %v", err)
	}

	wantPuts := []replayPut{
		{key: "seg0-k1", value: "v1", seq: 0},
		{key: "seg1-k1", value: "v1", seq: 1},
		{key: "seg2-k1", value: "v1", seq: 2},
	}
	if !reflect.DeepEqual(replayer.puts, wantPuts) {
		t.Errorf("puts = %#v, want %#v", replayer.puts, wantPuts)
	}
	if result.NextSequence != 3 {
		t.Errorf("NextSequence = %d, want 3", result.NextSequence)
	}
	if result.NextSegmentID != 3 {
		t.Errorf("NextSegmentID = %d, want 3", result.NextSegmentID)
	}
}

func fileExists(t *testing.T, path string) bool {
	t.Helper()
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	if os.IsNotExist(err) {
		return false
	}
	t.Fatalf("stat %s: %v", path, err)
	return false
}
