package logbuffer

import (
	"context"
	"strconv"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)

type TestEntry = vagrant_server.LogBatch_Entry

func TestBuffer(t *testing.T) {
	require := require.New(t)

	b := New()
	defer b.Close()

	// Get a reader
	r1 := b.Reader(-1)

	// Write some entries
	b.Write(nil, nil, nil)

	// The reader should be able to get three immediately
	v := r1.Read(10, true)
	require.Len(v, 3)
	require.Equal(3, cap(v))

	// We should block on the next read
	doneCh := make(chan struct{})
	go func() {
		defer close(doneCh)
		v = r1.Read(10, true)
	}()

	select {
	case <-doneCh:
		t.Fatal("should block")
	case <-time.After(50 * time.Millisecond):
	}

	// If we request a non-blocking we should get nil
	v = r1.Read(10, false)
	require.Nil(v)

	// Write some more entries which should unblock our reader
	b.Write(nil, nil)
	select {
	case <-doneCh:
	case <-time.After(50 * time.Millisecond):
		t.Fatal("should unblock")
	}

	// Write some more to verify non-blocking reads work
	b.Write(nil, nil, nil, nil)
	v = r1.Read(10, false)
	require.Len(v, 4)
}

func TestBuffer_close(t *testing.T) {
	require := require.New(t)

	b := New()
	defer b.Close()

	// Get a reader
	r1 := b.Reader(-1)

	// We should block on the next read
	doneCh := make(chan struct{})
	go func() {
		defer close(doneCh)
		r1.Read(10, true)
	}()

	select {
	case <-doneCh:
		t.Fatal("should block")
	case <-time.After(50 * time.Millisecond):
	}

	// Close our buffer
	require.NoError(b.Close())

	// Should be done
	select {
	case <-doneCh:
	case <-time.After(50 * time.Millisecond):
		t.Fatal("should not block")
	}

	// Should be safe to run
	require.NoError(r1.Close())
}

func TestBuffer_readPartial(t *testing.T) {
	require := require.New(t)

	b := New()
	defer b.Close()

	// Get a reader
	r1 := b.Reader(-1)

	// Write some entries
	b.Write(nil, nil, nil)

	{
		// Get two immediately
		v := r1.Read(2, true)
		require.Len(v, 2)
		require.Equal(2, cap(v))
	}

	{
		// Get the last one
		v := r1.Read(1, true)
		require.Len(v, 1)
		require.Equal(1, cap(v))
	}
}

func TestBuffer_writeFull(t *testing.T) {
	require := require.New(t)

	// Tiny chunks
	chchunk(t, 2, 2)

	// Create a buffer and write a bunch of data. This should overflow easily.
	// We want to verify we don't block or crash.
	b := New()
	defer b.Close()
	for i := 0; i < 53; i++ {
		b.Write(&TestEntry{
			Line: strconv.Itoa(i),
		})
	}

	// Get a reader and get what we can
	r := b.Reader(-1)
	vs := r.Read(10, true)
	require.NotEmpty(vs)
	require.Equal("52", vs[len(vs)-1].(*TestEntry).Line)
}

func TestBuffer_readFull(t *testing.T) {
	require := require.New(t)

	// Tiny chunks
	chchunk(t, 2, 1)

	// Create a buffer and get a reader immediately so we snapshot our
	// current set of buffers.
	b := New()
	defer b.Close()
	r := b.Reader(-1)

	// Write a lot of data to ensure we move the window
	for i := 0; i < 53; i++ {
		b.Write(&TestEntry{
			Line: strconv.Itoa(i),
		})
	}

	// Read the data
	vs := r.Read(1, true)
	require.NotEmpty(vs)
	require.Equal("0", vs[0].(*TestEntry).Line)

	vs = r.Read(1, true)
	require.NotEmpty(vs)
	require.Equal("1", vs[0].(*TestEntry).Line)

	// We jump windows here
	vs = r.Read(1, true)
	require.NotEmpty(vs)
	require.Equal("52", vs[0].(*TestEntry).Line)
}

func TestReader_cancel(t *testing.T) {
	require := require.New(t)

	b := New()
	defer b.Close()

	// Get a reader
	r1 := b.Reader(-1)

	// We should block on the read
	doneCh := make(chan struct{})
	go func() {
		defer close(doneCh)
		r1.Read(10, true)
	}()

	select {
	case <-doneCh:
		t.Fatal("should block")
	case <-time.After(50 * time.Millisecond):
	}

	// Close
	require.NoError(r1.Close())

	// Should return
	select {
	case <-doneCh:
	case <-time.After(50 * time.Millisecond):
		t.Fatal("should not block")
	}

	// Should be safe to call again
	require.NoError(r1.Close())
}

func TestReader_cancelContext(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())

	// Get a reader
	b := New()
	defer b.Close()
	r1 := b.Reader(-1)
	go r1.CloseContext(ctx)

	// We should block on the read
	doneCh := make(chan struct{})
	go func() {
		defer close(doneCh)
		r1.Read(10, true)
	}()

	select {
	case <-doneCh:
		t.Fatal("should block")
	case <-time.After(50 * time.Millisecond):
	}

	// Close
	cancel()

	// Should return
	select {
	case <-doneCh:
	case <-time.After(50 * time.Millisecond):
		t.Fatal("should not block")
	}
}

func TestBuffer_noHistory(t *testing.T) {
	b := New()
	defer b.Close()

	// Write some entries
	b.Write(nil, nil, nil)

	// Get a reader with no history. Should block.
	r1 := b.Reader(0)

	// Should block
	doneCh := make(chan struct{})
	go func() {
		defer close(doneCh)
		r1.Read(10, true)
	}()

	select {
	case <-doneCh:
		t.Fatal("should block")
	case <-time.After(50 * time.Millisecond):
	}

	// Write some more entries which should unblock our reader
	b.Write(nil, nil)
	select {
	case <-doneCh:
	case <-time.After(50 * time.Millisecond):
		t.Fatal("should unblock")
	}
}

func TestBuffer_maxHistory(t *testing.T) {
	require := require.New(t)

	b := New()
	defer b.Close()

	// Write some entries
	b.Write(1, 2, 3, 4, 5)

	// Get a reader with a max history set
	r1 := b.Reader(2)

	// The reader should be able to get maxHistory immediately
	v := r1.Read(10, true)
	require.Len(v, 2)
	require.Equal(2, cap(v))

	// We should block on the next read
	doneCh := make(chan struct{})
	go func() {
		defer close(doneCh)
		v = r1.Read(10, true)
	}()

	select {
	case <-doneCh:
		t.Fatal("should block")
	case <-time.After(50 * time.Millisecond):
	}

	// Write some more to verify non-blocking reads work
	b.Write(nil, nil, nil, nil)
	v = r1.Read(10, false)
	require.Len(v, 4)
}

func TestBuffer_maxHistoryBefore(t *testing.T) {
	require := require.New(t)

	b := New()
	defer b.Close()

	// Get a reader with a max history set
	r1 := b.Reader(2)

	// Write some entries
	b.Write(1, 2, 3, 4, 5)

	// The reader should be able to get maxHistory immediately
	v := r1.Read(10, true)
	require.Len(v, 5)
	require.Equal(5, cap(v))
}

func TestBuffer_maxHistoryMultiChunk(t *testing.T) {
	require := require.New(t)

	// Tiny chunks
	chchunk(t, 3, 4)

	// Create a buffer
	b := New()
	defer b.Close()

	// Write a lot of data to ensure we move the window
	for i := 0; i < 23; i++ {
		b.Write(i)
	}

	// Get a reader
	r := b.Reader(9)

	// Read the data
	var acc []Entry
	for {
		vs := r.Read(9, false)
		if vs == nil {
			break
		}

		acc = append(acc, vs...)
	}
	require.Len(acc, 9)
	require.Equal(acc[len(acc)-1], 22)
	require.Equal(acc[len(acc)-2], 21)
}

func chchunk(t *testing.T, count, size int) {
	oldcount, oldsize := chunkCount, chunkSize
	t.Cleanup(func() {
		chunkCount = oldcount
		chunkSize = oldsize
	})

	chunkCount, chunkSize = count, size
}
