/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package state

import (
	"crypto/rand"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	proto "github.com/hyperledger/fabric-protos-go-apiv2/gossip"
	"github.com/hyperledger/fabric/gossip/util"
	"github.com/stretchr/testify/require"
)

func init() {
	util.SetupTestLogging()
}

func randomPayloadWithSeqNum(seqNum uint64) (*proto.Payload, error) {
	data := make([]byte, 64)
	_, err := rand.Read(data)
	if err != nil {
		return nil, err
	}
	return &proto.Payload{
		SeqNum: seqNum,
		Data:   data,
	}, nil
}

func TestNewPayloadsBuffer(t *testing.T) {
	payloadsBuffer := NewPayloadsBuffer(10)
	require.Equal(t, payloadsBuffer.Next(), uint64(10))
}

func TestPayloadsBufferImpl_Push(t *testing.T) {
	buffer := NewPayloadsBuffer(5)

	payload, err := randomPayloadWithSeqNum(4)
	if err != nil {
		t.Fatal("Wasn't able to generate random payload for test")
	}

	t.Log("Pushing new payload into buffer")
	buffer.Push(payload)

	// Payloads with sequence number less than buffer top
	// index should not be accepted
	t.Log("Getting next block sequence number")
	require.Equal(t, buffer.Next(), uint64(5))
	t.Log("Check block buffer size")
	require.Equal(t, buffer.Size(), 0)

	// Adding new payload with seq. number equal to top
	// payload should not be added
	payload, err = randomPayloadWithSeqNum(5)
	if err != nil {
		t.Fatal("Wasn't able to generate random payload for test")
	}

	t.Log("Pushing new payload into buffer")
	buffer.Push(payload)
	t.Log("Getting next block sequence number")
	require.Equal(t, buffer.Next(), uint64(5))
	t.Log("Check block buffer size")
	require.Equal(t, buffer.Size(), 1)
}

func TestPayloadsBufferImpl_Ready(t *testing.T) {
	fin := make(chan struct{})
	buffer := NewPayloadsBuffer(1)
	require.Equal(t, buffer.Next(), uint64(1))

	go func() {
		<-buffer.Ready()
		fin <- struct{}{}
	}()

	time.AfterFunc(100*time.Millisecond, func() {
		payload, err := randomPayloadWithSeqNum(1)
		if err != nil {
			t.Fatal("Wasn't able to generate random payload for test")
		}
		buffer.Push(payload)
	})

	select {
	case <-fin:
		payload := buffer.Pop()
		require.Equal(t, payload.SeqNum, uint64(1))
	case <-time.After(500 * time.Millisecond):
		t.Fail()
	}
}

// Test to push several concurrent blocks into the buffer
// with same sequence number, only one expected to succeed
func TestPayloadsBufferImpl_ConcurrentPush(t *testing.T) {
	// Test setup, next block num to expect and
	// how many concurrent pushes to simulate
	nextSeqNum := uint64(7)
	concurrency := 10

	buffer := NewPayloadsBuffer(nextSeqNum)
	require.Equal(t, buffer.Next(), nextSeqNum)

	startWG := sync.WaitGroup{}
	startWG.Add(1)

	finishWG := sync.WaitGroup{}
	finishWG.Add(concurrency)

	payload, err := randomPayloadWithSeqNum(nextSeqNum)
	require.NoError(t, err)

	ready := int32(0)
	readyWG := sync.WaitGroup{}
	readyWG.Add(1)
	go func() {
		// Wait for next expected block to arrive
		<-buffer.Ready()
		atomic.AddInt32(&ready, 1)
		readyWG.Done()
	}()

	for i := 0; i < concurrency; i++ {
		go func() {
			buffer.Push(payload)
			startWG.Wait()
			finishWG.Done()
		}()
	}
	startWG.Done()
	finishWG.Wait()

	readyWG.Wait()
	require.Equal(t, int32(1), atomic.LoadInt32(&ready))
	// Buffer size has to be only one
	require.Equal(t, 1, buffer.Size())
}

// Tests the scenario where payload pushes and pops are interleaved after a Ready() signal.
func TestPayloadsBufferImpl_Interleave(t *testing.T) {
	buffer := NewPayloadsBuffer(1)
	require.Equal(t, buffer.Next(), uint64(1))

	//
	// First two sequences arrives and the buffer is emptied without interleave.
	//
	// This is also an example of the produce/consumer pattern in Fabric.
	// Producer:
	//
	// Payloads are pushed into the buffer. These payloads can be out of order.
	// When the buffer has a sequence of payloads ready (in order), it fires a signal
	// on it's Ready() channel.
	//
	// The consumer waits for the signal and then drains all ready payloads.

	payload, err := randomPayloadWithSeqNum(1)
	require.NoError(t, err, "generating random payload failed")
	buffer.Push(payload)

	payload, err = randomPayloadWithSeqNum(2)
	require.NoError(t, err, "generating random payload failed")
	buffer.Push(payload)

	select {
	case <-buffer.Ready():
	case <-time.After(500 * time.Millisecond):
		t.Error("buffer wasn't ready after 500 ms for first sequence")
	}

	// The consumer empties the buffer.
	for payload := buffer.Pop(); payload != nil; payload = buffer.Pop() {
	}

	// The buffer isn't ready since no new sequences have come since emptying the buffer.
	select {
	case <-buffer.Ready():
		t.Error("buffer should not be ready as no new sequences have come")
	case <-time.After(500 * time.Millisecond):
	}

	//
	// Next sequences are incoming at the same time the buffer is being emptied by the consumer.
	//
	payload, err = randomPayloadWithSeqNum(3)
	require.NoError(t, err, "generating random payload failed")
	buffer.Push(payload)

	select {
	case <-buffer.Ready():
	case <-time.After(500 * time.Millisecond):
		t.Error("buffer wasn't ready after 500 ms for second sequence")
	}
	payload = buffer.Pop()
	require.NotNil(t, payload, "payload should not be nil")

	// ... Block processing now happens on sequence 3.

	// In the mean time, sequence 4 is pushed into the queue.
	payload, err = randomPayloadWithSeqNum(4)
	require.NoError(t, err, "generating random payload failed")
	buffer.Push(payload)

	// ... Block processing completes on sequence 3, the consumer loop grabs the next one (4).
	payload = buffer.Pop()
	require.NotNil(t, payload, "payload should not be nil")

	// In the mean time, sequence 5 is pushed into the queue.
	payload, err = randomPayloadWithSeqNum(5)
	require.NoError(t, err, "generating random payload failed")
	buffer.Push(payload)

	// ... Block processing completes on sequence 4, the consumer loop grabs the next one (5).
	payload = buffer.Pop()
	require.NotNil(t, payload, "payload should not be nil")

	//
	// Now we see that goroutines are building up due to the interleaved push and pops above.
	//
	select {
	case <-buffer.Ready():
		//
		// Should be error - no payloads are ready
		//
		t.Log("buffer ready (1) -- should be error")
		t.Fail()
	case <-time.After(500 * time.Millisecond):
		t.Log("buffer not ready (1)")
	}
	payload = buffer.Pop()
	t.Logf("payload: %v", payload)
	require.Nil(t, payload, "payload should be nil")

	select {
	case <-buffer.Ready():
		//
		// Should be error - no payloads are ready
		//
		t.Log("buffer ready (2) -- should be error")
		t.Fail()
	case <-time.After(500 * time.Millisecond):
		t.Log("buffer not ready (2)")
	}
	payload = buffer.Pop()
	require.Nil(t, payload, "payload should be nil")
	t.Logf("payload: %v", payload)

	select {
	case <-buffer.Ready():
		t.Error("buffer ready (3)")
	case <-time.After(500 * time.Millisecond):
		t.Log("buffer not ready (3) -- good")
	}
}
