package p2p

import (
	"math"
	"math/rand"
	"testing"
	"time"

	gogotypes "github.com/gogo/protobuf/types"
	"github.com/stretchr/testify/require"
	"github.com/tendermint/tendermint/libs/log"
	tmsync "github.com/tendermint/tendermint/libs/sync"
)

type testMessage = gogotypes.StringValue

func TestWDRRQueue_EqualWeights(t *testing.T) {
	chDescs := []ChannelDescriptor{
		{ID: 0x01, Priority: 1, MaxSendBytes: 4},
		{ID: 0x02, Priority: 1, MaxSendBytes: 4},
		{ID: 0x03, Priority: 1, MaxSendBytes: 4},
		{ID: 0x04, Priority: 1, MaxSendBytes: 4},
		{ID: 0x05, Priority: 1, MaxSendBytes: 4},
		{ID: 0x06, Priority: 1, MaxSendBytes: 4},
	}

	peerQueue := newWDRRScheduler(log.NewNopLogger(), NopMetrics(), chDescs, 1000, 1000, 120)
	peerQueue.start()

	totalMsgs := make(map[ChannelID]int)
	deliveredMsgs := make(map[ChannelID]int)
	successRates := make(map[ChannelID]float64)

	closer := tmsync.NewCloser()

	go func() {
		timout := 10 * time.Second
		ticker := time.NewTicker(timout)
		defer ticker.Stop()

		for {
			select {
			case e := <-peerQueue.dequeue():
				deliveredMsgs[e.channelID]++
				ticker.Reset(timout)

			case <-ticker.C:
				closer.Close()
			}
		}
	}()

	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
	maxMsgs := 5000
	minMsgs := 1000

	for _, chDesc := range chDescs {
		total := rng.Intn(maxMsgs-minMsgs) + minMsgs // total = rand[minMsgs, maxMsgs)
		totalMsgs[ChannelID(chDesc.ID)] = total

		go func(cID ChannelID, n int) {
			for i := 0; i < n; i++ {
				peerQueue.enqueue() <- Envelope{
					channelID: cID,
					Message:   &testMessage{Value: "foo"}, // 5 bytes
				}
			}
		}(ChannelID(chDesc.ID), total)
	}

	// wait for dequeueing to complete
	<-closer.Done()

	// close queue and wait for cleanup
	peerQueue.close()
	<-peerQueue.closed()

	var (
		sum    float64
		stdDev float64
	)

	for _, chDesc := range peerQueue.chDescs {
		chID := ChannelID(chDesc.ID)
		require.Zero(t, peerQueue.deficits[chID], "expected flow deficit to be zero")
		require.Len(t, peerQueue.buffer[chID], 0, "expected flow queue to be empty")

		total := totalMsgs[chID]
		delivered := deliveredMsgs[chID]
		successRate := float64(delivered) / float64(total)

		sum += successRate
		successRates[chID] = successRate

		// require some messages dropped
		require.Less(t, delivered, total, "expected some messages to be dropped")
		require.Less(t, successRate, 1.0, "expected a success rate below 100%")
	}

	require.Zero(t, peerQueue.size, "expected scheduler size to be zero")

	numFlows := float64(len(peerQueue.buffer))
	mean := sum / numFlows

	for _, successRate := range successRates {
		stdDev += math.Pow(successRate-mean, 2)
	}

	stdDev = math.Sqrt(stdDev / numFlows)
	require.Less(t, stdDev, 0.02, "expected success rate standard deviation to be less than 2%")
}

func TestWDRRQueue_DecreasingWeights(t *testing.T) {
	chDescs := []ChannelDescriptor{
		{ID: 0x01, Priority: 18, MaxSendBytes: 4},
		{ID: 0x02, Priority: 10, MaxSendBytes: 4},
		{ID: 0x03, Priority: 2, MaxSendBytes: 4},
		{ID: 0x04, Priority: 1, MaxSendBytes: 4},
		{ID: 0x05, Priority: 1, MaxSendBytes: 4},
		{ID: 0x06, Priority: 1, MaxSendBytes: 4},
	}

	peerQueue := newWDRRScheduler(log.NewNopLogger(), NopMetrics(), chDescs, 0, 0, 500)
	peerQueue.start()

	totalMsgs := make(map[ChannelID]int)
	deliveredMsgs := make(map[ChannelID]int)
	successRates := make(map[ChannelID]float64)

	for _, chDesc := range chDescs {
		total := 1000
		totalMsgs[ChannelID(chDesc.ID)] = total

		go func(cID ChannelID, n int) {
			for i := 0; i < n; i++ {
				peerQueue.enqueue() <- Envelope{
					channelID: cID,
					Message:   &testMessage{Value: "foo"}, // 5 bytes
				}
			}
		}(ChannelID(chDesc.ID), total)
	}

	closer := tmsync.NewCloser()

	go func() {
		timout := 20 * time.Second
		ticker := time.NewTicker(timout)
		defer ticker.Stop()

		for {
			select {
			case e := <-peerQueue.dequeue():
				deliveredMsgs[e.channelID]++
				ticker.Reset(timout)

			case <-ticker.C:
				closer.Close()
			}
		}
	}()

	// wait for dequeueing to complete
	<-closer.Done()

	// close queue and wait for cleanup
	peerQueue.close()
	<-peerQueue.closed()

	for i, chDesc := range peerQueue.chDescs {
		chID := ChannelID(chDesc.ID)
		require.Zero(t, peerQueue.deficits[chID], "expected flow deficit to be zero")
		require.Len(t, peerQueue.buffer[chID], 0, "expected flow queue to be empty")

		total := totalMsgs[chID]
		delivered := deliveredMsgs[chID]
		successRate := float64(delivered) / float64(total)

		successRates[chID] = successRate

		// Require some messages dropped. Note, the top weighted flows may not have
		// any dropped if lower priority non-empty queues always exist.
		if i > 2 {
			require.Less(t, delivered, total, "expected some messages to be dropped")
			require.Less(t, successRate, 1.0, "expected a success rate below 100%")
		}
	}

	require.Zero(t, peerQueue.size, "expected scheduler size to be zero")

	// require channel 0x01 to have the highest success rate due to its weight
	ch01Rate := successRates[ChannelID(chDescs[0].ID)]
	for i := 1; i < len(chDescs); i++ {
		require.GreaterOrEqual(t, ch01Rate, successRates[ChannelID(chDescs[i].ID)])
	}

	// require channel 0x02 to have the 2nd highest success rate due to its weight
	ch02Rate := successRates[ChannelID(chDescs[1].ID)]
	for i := 2; i < len(chDescs); i++ {
		require.GreaterOrEqual(t, ch02Rate, successRates[ChannelID(chDescs[i].ID)])
	}

	// require channel 0x03 to have the 3rd highest success rate due to its weight
	ch03Rate := successRates[ChannelID(chDescs[2].ID)]
	for i := 3; i < len(chDescs); i++ {
		require.GreaterOrEqual(t, ch03Rate, successRates[ChannelID(chDescs[i].ID)])
	}
}
