package wal

import (
	"errors"
	"fmt"
	"sync"
	"testing"

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

func newTestWalWriter(t *testing.T) *WalWriter {
	t.Helper()

	cfg := config.Defaults()
	ww, err := NewWalWriter(&cfg, t.TempDir(), 0, 0)
	if err != nil {
		t.Fatalf("NewWalWriter: %v", err)
	}
	t.Cleanup(func() {
		if err := ww.Close(); err != nil {
			t.Fatalf("Close: %v", err)
		}
	})

	return ww
}

func TestWalWriterSinglePut(t *testing.T) {
	ww := newTestWalWriter(t)

	if err := ww.Put([]byte("k1"), []byte("v1")); err != nil {
		t.Fatalf("Put: %v", err)
	}

	got := ww.Get([]byte("k1"))
	if !got.Found {
		t.Fatal("Get(k1) not found")
	}
	if string(got.Value) != "v1" {
		t.Fatalf("Get(k1) value = %q, want %q", got.Value, "v1")
	}
}

func TestWalWriterMultiplePuts(t *testing.T) {
	ww := newTestWalWriter(t)

	for i := range 10 {
		key := fmt.Appendf(nil, "k%d", i)
		value := fmt.Appendf(nil, "v%d", i)
		if err := ww.Put(key, value); err != nil {
			t.Fatalf("Put(%q): %v", key, err)
		}
	}

	for i := range 10 {
		key := fmt.Appendf(nil, "k%d", i)
		want := fmt.Sprintf("v%d", i)
		got := ww.Get(key)
		if !got.Found {
			t.Fatalf("Get(%q) not found", key)
		}
		if string(got.Value) != want {
			t.Fatalf("Get(%q) value = %q, want %q", key, got.Value, want)
		}
	}
}

func TestWalWriterGroupCommit(t *testing.T) {
	ww := newTestWalWriter(t)

	const writers = 5
	var wg sync.WaitGroup
	errCh := make(chan error, writers)

	for i := range writers {
		wg.Go(func() {
			key := fmt.Appendf(nil, "group-k%d", i)
			value := fmt.Appendf(nil, "group-v%d", i)
			if err := ww.Put(key, value); err != nil {
				errCh <- fmt.Errorf("put %d: %w", i, err)
			}
		})
	}

	wg.Wait()
	close(errCh)
	for err := range errCh {
		if err != nil {
			t.Fatal(err)
		}
	}

	for i := range writers {
		key := fmt.Appendf(nil, "group-k%d", i)
		want := fmt.Sprintf("group-v%d", i)
		got := ww.Get(key)
		if !got.Found {
			t.Fatalf("Get(%q) not found", key)
		}
		if string(got.Value) != want {
			t.Fatalf("Get(%q) value = %q, want %q", key, got.Value, want)
		}
	}
}

func TestWalWriterDelete(t *testing.T) {
	ww := newTestWalWriter(t)

	if err := ww.Put([]byte("k"), []byte("v")); err != nil {
		t.Fatalf("Put: %v", err)
	}
	if err := ww.Delete([]byte("k")); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	got := ww.Get([]byte("k"))
	if got.Found {
		t.Fatalf("Get(k) found deleted key with value %q", got.Value)
	}
}

// TestWriteStoppedAfterIOError verifies: I/O failure during AppendBatch →
// ErrCommitUnknown returned, write-stopped state entered, subsequent writes
// return ErrWriteStopped.
func TestWriteStoppedAfterIOError(t *testing.T) {
	cfg := config.Defaults()
	ww, err := NewWalWriter(&cfg, t.TempDir(), 0, 0)
	if err != nil {
		t.Fatalf("NewWalWriter: %v", err)
	}

	if err := ww.Put([]byte("ok-key"), []byte("ok-val")); err != nil {
		t.Fatalf("initial Put: %v", err)
	}

	// Close the fd to force AppendBatch → stopWithError(ErrCommitUnknown).
	if err := ww.segManager.active.fd.Close(); err != nil {
		t.Fatalf("close fd: %v", err)
	}

	err = ww.Put([]byte("fail-key"), []byte("fail-val"))
	if err == nil {
		t.Fatal("expected error from Put after I/O failure, got nil")
	}
	if !errors.Is(err, errkit.ErrCommitUnknown) {
		t.Errorf("Put error = %v, want ErrCommitUnknown", err)
	}

	if !ww.IsWriteStopped() {
		t.Fatal("IsWriteStopped() = false, want true after I/O error")
	}

	err = ww.Put([]byte("after-key"), []byte("after-val"))
	if !errors.Is(err, errkit.ErrWriteStopped) {
		t.Errorf("Put after write-stopped error = %v, want ErrWriteStopped", err)
	}

	err = ww.Delete([]byte("after-key"))
	if !errors.Is(err, errkit.ErrWriteStopped) {
		t.Errorf("Delete after write-stopped error = %v, want ErrWriteStopped", err)
	}

	_ = ww.Close()
}
