package s3client

import (
	"bytes"
	"context"
	"errors"
	"io"
	"strings"
	"testing"
)

func TestLocalAppendLogPutRequiresExactBodySize(t *testing.T) {
	c, err := NewLocal(t.TempDir(), "")
	if err != nil {
		t.Fatalf("NewLocal: %v", err)
	}
	ctx := context.Background()
	if err := c.PutAppendLogObject(ctx, "append-log/test", bytes.NewReader([]byte("old")), 3); err != nil {
		t.Fatalf("seed append-log object: %v", err)
	}
	if err := c.PutAppendLogObject(ctx, "append-log/test", bytes.NewReader([]byte("too long")), 3); err == nil {
		t.Fatal("PutAppendLogObject accepted an oversized body")
	}
	rc, err := c.GetObject(ctx, "append-log/test")
	if err != nil {
		t.Fatalf("read existing append-log object: %v", err)
	}
	defer func() { _ = rc.Close() }()
	got, err := io.ReadAll(rc)
	if err != nil {
		t.Fatalf("read existing append-log data: %v", err)
	}
	if string(got) != "old" {
		t.Fatalf("object after rejected replace = %q, want old", got)
	}
}

func TestLocalAppendLogAppendRequiresExactBodySize(t *testing.T) {
	c, err := NewLocal(t.TempDir(), "")
	if err != nil {
		t.Fatalf("NewLocal: %v", err)
	}
	ctx := context.Background()
	if err := c.PutAppendLogObject(ctx, "append-log/test", bytes.NewReader([]byte("old")), 3); err != nil {
		t.Fatalf("seed append-log object: %v", err)
	}
	if err := c.AppendObject(ctx, "append-log/test", bytes.NewReader([]byte("too long")), 3, 3); err == nil {
		t.Fatal("AppendObject accepted an oversized body")
	}
	rc, err := c.GetObject(ctx, "append-log/test")
	if err != nil {
		t.Fatalf("read append-log object: %v", err)
	}
	defer func() { _ = rc.Close() }()
	got, err := io.ReadAll(rc)
	if err != nil {
		t.Fatalf("read append-log data: %v", err)
	}
	if string(got) != "old" {
		t.Fatalf("object after rejected append = %q, want old", got)
	}
}

func TestLocalAppendLogAppendSerializesOffsetCAS(t *testing.T) {
	c, err := NewLocal(t.TempDir(), "")
	if err != nil {
		t.Fatalf("NewLocal: %v", err)
	}
	ctx := context.Background()
	const (
		key  = "append-log/test"
		seed = "seed"
	)
	if err := c.PutAppendLogObject(ctx, key, strings.NewReader(seed), int64(len(seed))); err != nil {
		t.Fatalf("seed append-log object: %v", err)
	}

	type appendResult struct {
		tail string
		err  error
	}
	ready := make(chan struct{}, 2)
	start := make(chan struct{})
	results := make(chan appendResult, 2)
	for _, tail := range []string{"one", "two"} {
		go func() {
			ready <- struct{}{}
			<-start
			results <- appendResult{
				tail: tail,
				err:  c.AppendObject(ctx, key, strings.NewReader(tail), int64(len(tail)), int64(len(seed))),
			}
		}()
	}
	<-ready
	<-ready
	close(start)

	var accepted string
	for range 2 {
		result := <-results
		if result.err == nil {
			if accepted != "" {
				t.Fatalf("multiple same-offset appends succeeded: %q and %q", accepted, result.tail)
			}
			accepted = result.tail
			continue
		}
		if !errors.Is(result.err, ErrAppendOffsetMismatch) {
			t.Fatalf("same-offset append error = %v, want ErrAppendOffsetMismatch", result.err)
		}
	}
	if accepted == "" {
		t.Fatal("no same-offset append succeeded")
	}

	rc, err := c.GetObject(ctx, key)
	if err != nil {
		t.Fatalf("read append-log object: %v", err)
	}
	defer func() { _ = rc.Close() }()
	got, err := io.ReadAll(rc)
	if err != nil {
		t.Fatalf("read append-log data: %v", err)
	}
	if want := seed + accepted; string(got) != want {
		t.Fatalf("object after same-offset appends = %q, want exactly one accepted tail %q", got, want)
	}
}

func TestLocalAppendLogFaultsAreDeterministic(t *testing.T) {
	c, err := NewLocal(t.TempDir(), "")
	if err != nil {
		t.Fatalf("NewLocal: %v", err)
	}
	ctx := context.Background()
	if err := c.PutAppendLogObject(ctx, "append-log/test", bytes.NewReader([]byte("seed")), 4); err != nil {
		t.Fatalf("seed append-log object: %v", err)
	}
	c.SetAppendLogFaults(AppendLogFaults{AppendErr: ErrAppendTooManyParts})
	if err := c.AppendObject(ctx, "append-log/test", bytes.NewReader([]byte("x")), 1, 4); !errors.Is(err, ErrAppendTooManyParts) {
		t.Fatalf("append injected error = %v, want ErrAppendTooManyParts", err)
	}
	c.SetAppendLogFaults(AppendLogFaults{CopyErr: ErrAppendOffsetMismatch})
	if err := c.CopyAppendLogObject(ctx, "append-log/test", "append-log/copy"); !errors.Is(err, ErrAppendOffsetMismatch) {
		t.Fatalf("copy injected error = %v, want ErrAppendOffsetMismatch", err)
	}
}

func TestLocalAppendLogPutAcceptsNilEmptyBody(t *testing.T) {
	c, err := NewLocal(t.TempDir(), "")
	if err != nil {
		t.Fatalf("NewLocal: %v", err)
	}
	if err := c.PutAppendLogObject(context.Background(), "append-log/empty", nil, 0); err != nil {
		t.Fatalf("PutAppendLogObject(nil, 0): %v", err)
	}
	rc, err := c.GetObject(context.Background(), "append-log/empty")
	if err != nil {
		t.Fatalf("read empty append-log object: %v", err)
	}
	defer func() { _ = rc.Close() }()
	data, err := io.ReadAll(rc)
	if err != nil {
		t.Fatalf("read empty append-log object: %v", err)
	}
	if len(data) != 0 {
		t.Fatalf("empty append-log object length = %d, want 0", len(data))
	}
}
