package relay

import (
	"bytes"
	"context"
	"errors"
	"io"
	"sync"
	"testing"
	"time"
)

func TestRing_BasicWriteRead(t *testing.T) {
	r := newRing(64)
	want := []byte("hello, world")
	if n, err := r.Write(context.Background(), want); err != nil || n != len(want) {
		t.Fatalf("write: n=%d err=%v", n, err)
	}
	got := make([]byte, 32)
	n, err := r.Read(context.Background(), got)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !bytes.Equal(got[:n], want) {
		t.Errorf("read mismatch: got %q want %q", got[:n], want)
	}
}

func TestRing_CloseWriterSignalsEOF(t *testing.T) {
	r := newRing(32)
	_, _ = r.Write(context.Background(), []byte("abc"))
	r.CloseWriter()

	got := make([]byte, 16)
	n, err := r.Read(context.Background(), got)
	if err != nil || n != 3 {
		t.Fatalf("first read: n=%d err=%v", n, err)
	}
	n, err = r.Read(context.Background(), got)
	if !errors.Is(err, io.EOF) {
		t.Errorf("expected EOF after drain; got n=%d err=%v", n, err)
	}
}

func TestRing_AbortReturnsErrClosed(t *testing.T) {
	r := newRing(32)
	r.Abort()
	if _, err := r.Write(context.Background(), []byte("x")); !errors.Is(err, ErrClosed) {
		t.Errorf("write after abort: got %v want ErrClosed", err)
	}
	if _, err := r.Read(context.Background(), make([]byte, 1)); !errors.Is(err, ErrClosed) {
		t.Errorf("read after abort: got %v want ErrClosed", err)
	}
}

func TestRing_BlocksWhenFullThenUnblocksOnRead(t *testing.T) {
	r := newRing(8)
	if _, err := r.Write(context.Background(), []byte("12345678")); err != nil {
		t.Fatalf("fill: %v", err)
	}

	done := make(chan struct{})
	go func() {
		_, err := r.Write(context.Background(), []byte("X"))
		if err != nil {
			t.Errorf("blocked write: %v", err)
		}
		close(done)
	}()

	// Without a reader the goroutine should block.
	select {
	case <-done:
		t.Fatal("write should block when ring is full")
	case <-time.After(50 * time.Millisecond):
	}

	got := make([]byte, 4)
	if _, err := r.Read(context.Background(), got); err != nil {
		t.Fatalf("read: %v", err)
	}
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("write did not unblock after read")
	}
}

func TestRing_CtxCancelsBlockedRead(t *testing.T) {
	r := newRing(8)
	ctx, cancel := context.WithCancel(context.Background())

	type res struct {
		n   int
		err error
	}
	out := make(chan res, 1)
	go func() {
		buf := make([]byte, 4)
		n, err := r.Read(ctx, buf)
		out <- res{n, err}
	}()

	time.Sleep(20 * time.Millisecond)
	cancel()

	select {
	case r := <-out:
		if !errors.Is(r.err, context.Canceled) {
			t.Errorf("expected context.Canceled, got %v", r.err)
		}
	case <-time.After(time.Second):
		t.Fatal("blocked read did not unblock on ctx cancel")
	}
}

func TestRing_WrapsAround(t *testing.T) {
	r := newRing(4)
	// Cycle through enough bytes to force head/tail wrapping.
	for i := 0; i < 20; i++ {
		_, _ = r.Write(context.Background(), []byte{byte(i)})
		got := make([]byte, 1)
		n, _ := r.Read(context.Background(), got)
		if n != 1 || got[0] != byte(i) {
			t.Errorf("iter %d: got %v want [%d]", i, got, i)
		}
	}
}

func TestRing_StressManyChunks(t *testing.T) {
	const total = 4 * 1024
	r := newRing(64)

	var wgWriter, wgReader sync.WaitGroup
	wgWriter.Add(1)
	wgReader.Add(1)

	src := make([]byte, total)
	for i := range src {
		src[i] = byte(i)
	}

	go func() {
		defer wgWriter.Done()
		for i := 0; i < total; i += 13 {
			end := i + 13
			if end > total {
				end = total
			}
			_, _ = r.Write(context.Background(), src[i:end])
		}
		r.CloseWriter()
	}()

	got := make([]byte, 0, total)
	go func() {
		defer wgReader.Done()
		buf := make([]byte, 17)
		for {
			n, err := r.Read(context.Background(), buf)
			got = append(got, buf[:n]...)
			if errors.Is(err, io.EOF) {
				return
			}
			if err != nil {
				t.Errorf("read: %v", err)
				return
			}
		}
	}()

	wgWriter.Wait()
	wgReader.Wait()
	if !bytes.Equal(got, src) {
		t.Errorf("mismatch: got %d bytes, want %d", len(got), len(src))
	}
}
