/*
* Copyright 2021-2024 Open Networking Foundation (ONF) and the ONF Contributors

* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at

* http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
 */
package kvstore

import (
	"context"
	"os"
	"strconv"
	"sync"
	"testing"
	"time"

	"github.com/opencord/voltha-lib-go/v7/pkg/log"
	mocks "github.com/opencord/voltha-lib-go/v7/pkg/mocks/etcd"
	"github.com/phayes/freeport"
	"github.com/stretchr/testify/assert"
)

const (
	embedEtcdServerHost = "localhost"
	defaultTimeout      = 1 * time.Second
)

var (
	embedEtcdServerPort int
)

func TestMain(m *testing.M) {
	ctx := context.Background()
	var err error
	embedEtcdServerPort, err = freeport.GetFreePort()
	if err != nil {
		logger.Fatal(ctx, err)
	}
	peerPort, err := freeport.GetFreePort()
	if err != nil {
		logger.Fatal(ctx, err)
	}
	etcdServer := mocks.StartEtcdServer(ctx, mocks.MKConfig(ctx,
		"voltha.db.kvstore.test",
		embedEtcdServerPort,
		peerPort,
		"voltha.lib.db.kvstore",
		"error"))
	res := m.Run()

	etcdServer.Stop(ctx)
	os.Exit(res)
}

func TestNewRoundRobinEtcdClientAllocator(t *testing.T) {
	address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)
	capacity := 20
	maxUsage := 10

	pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
	assert.NotNil(t, pool)
	assert.Nil(t, err)
}

func TestRoundRobin_Get_Put(t *testing.T) {
	address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)
	capacity := 20
	maxUsage := 10

	pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
	assert.NotNil(t, pool)
	assert.Nil(t, err)

	// Verify we can obtain the expected number of clients with no errors or waiting time
	var wg sync.WaitGroup
	for i := 0; i < capacity*maxUsage; i++ {
		wg.Add(1)
		go func() {
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			c, err := pool.Get(ctx)
			assert.NotNil(t, c)
			assert.Nil(t, err)
			time.Sleep(5 * time.Second)
			pool.Put(c)
			wg.Done()
		}()
	}
	wg.Wait()
}

func TestRoundRobin_Get_Put_various_capacity(t *testing.T) {
	// Test single client with 1 concurrent usage
	getPutVaryingCapacity(t, 1, 1)

	// Test single client with multiple concurrent usage
	getPutVaryingCapacity(t, 1, 100)

	// Test multiple clients with single concurrent usage
	getPutVaryingCapacity(t, 10, 1)

	// Test multiple clients with multiple concurrent usage
	getPutVaryingCapacity(t, 10, 10)
}

func getPutVaryingCapacity(t *testing.T, capacity, maxUsage int) {
	address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)

	pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
	assert.NotNil(t, pool)
	assert.Nil(t, err)

	// Verify we can obtain the expected number of clients with no errors or waiting time
	var wg sync.WaitGroup
	totalSize := capacity * maxUsage
	ch := make(chan struct{})
	for i := 0; i < totalSize; i++ {
		wg.Add(1)
		go func() {
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			c, err := pool.Get(ctx)
			assert.NotNil(t, c)
			assert.Nil(t, err)
			// Inform the waiting loop that a client has been allocated
			ch <- struct{}{}
			// Keep the client for 5s and then return it to the pool
			time.Sleep(5 * time.Second)
			pool.Put(c)
			wg.Done()
		}()
	}

	// Wait until all clients are allocated
	allocated := 0
	for range ch {
		allocated++
		if allocated == totalSize {
			break
		}
	}

	// Try to get above capacity/usage with low timeout
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	c, err := pool.Get(ctx)
	assert.NotNil(t, err)
	assert.Nil(t, c)
	cancel()

	// Try to get above capacity/usage with longer timeout
	ctx, cancel = context.WithTimeout(context.Background(), 6*time.Second)
	c, err = pool.Get(ctx)
	assert.NotNil(t, c)
	assert.Nil(t, err)
	pool.Put(c)
	cancel()

	wg.Wait()

	// Close the connection
	pool.Close(context.Background())
}

func TestRoundRobin_Close_various_capacity(t *testing.T) {
	// Test single client with 1 concurrent usage
	closeWithVaryingCapacity(t, 1, 1)

	// Test single client with multiple concurrent usage
	closeWithVaryingCapacity(t, 1, 100)

	// Test multiple clients with single concurrent usage
	closeWithVaryingCapacity(t, 10, 1)

	// Test multiple clients with multiple concurrent usage
	closeWithVaryingCapacity(t, 10, 10)
}

func closeWithVaryingCapacity(t *testing.T, capacity, maxUsage int) {
	address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)

	pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
	assert.NotNil(t, pool)
	assert.Nil(t, err)

	// Verify we can obtain the expected number of clients with no errors or waiting time
	var wg sync.WaitGroup
	totalSize := capacity * maxUsage
	ch := make(chan struct{})
	for i := 0; i < totalSize; i++ {
		wg.Add(1)
		go func() {
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			c, err := pool.Get(ctx)
			assert.NotNil(t, c)
			assert.Nil(t, err)
			// Inform the waiting loop that a client has been allocated
			ch <- struct{}{}
			// Keep the client for 5s and then return it to the pool
			time.Sleep(5 * time.Second)
			pool.Put(c)
			wg.Done()
		}()
	}

	// Wait until all clients are allocated
	allocated := 0
	for range ch {
		allocated++
		if allocated == totalSize {
			break
		}
	}
	// Try to get above capacity/usage with longer timeout
	wg.Add(1)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		c, err := pool.Get(ctx)
		assert.NotNil(t, err)
		expected := err.Error() == "pool-is-closing" || err.Error() == "stop-waiting-pool-is-closing"
		assert.True(t, expected)
		assert.Nil(t, c)
		cancel()
		wg.Done()
	}()

	// Invoke close on the pool
	wg.Add(1)
	go func() {
		pool.Close(context.Background())
		wg.Done()
	}()

	// Try to get new client and ensure we can't get one
	c, err := pool.Get(context.Background())
	assert.NotNil(t, err)
	expected := err.Error() == "pool-is-closing" || err.Error() == "stop-waiting-pool-is-closing"
	assert.True(t, expected)
	assert.Nil(t, c)

	wg.Wait()
}
