package pool

import (
	"context"
	"fmt"
	"math/rand"
	"testing"

	"github.com/nspcc-dev/neofs-sdk-go/client"
	"github.com/stretchr/testify/require"
)

func TestSamplerStability(t *testing.T) {
	const COUNT = 100000

	cases := []struct {
		probabilities []float64
		expected      []int
	}{
		{
			probabilities: []float64{1, 0},
			expected:      []int{COUNT, 0},
		},
		{
			probabilities: []float64{0.1, 0.2, 0.7},
			expected:      []int{10138, 19813, 70049},
		},
		{
			probabilities: []float64{0.2, 0.2, 0.4, 0.1, 0.1, 0},
			expected:      []int{19824, 20169, 39900, 10243, 9864, 0},
		},
	}

	for _, tc := range cases {
		sampler := NewSampler(tc.probabilities, rand.NewSource(0))
		res := make([]int, len(tc.probabilities))
		for i := 0; i < COUNT; i++ {
			res[sampler.Next()]++
		}

		require.Equal(t, tc.expected, res, "probabilities: %v", tc.probabilities)
	}
}

type clientMock struct {
	client.Client
	name string
	err  error
}

func (c *clientMock) EndpointInfo(context.Context, client.PrmEndpointInfo) (*client.ResEndpointInfo, error) {
	return nil, nil
}

func (c *clientMock) NetworkInfo(context.Context, client.PrmNetworkInfo) (*client.ResNetworkInfo, error) {
	return nil, nil
}

func newNetmapMock(name string, needErr bool) *clientMock {
	var err error
	if needErr {
		err = fmt.Errorf("not available")
	}
	return &clientMock{name: name, err: err}
}

func TestHealthyReweight(t *testing.T) {
	var (
		weights = []float64{0.9, 0.1}
		names   = []string{"node0", "node1"}
		options = &BuilderOptions{nodesParams: []*NodesParam{{weights: weights}}}
		buffer  = make([]float64, len(weights))
	)

	cache, err := NewCache()
	require.NoError(t, err)

	inner := &innerPool{
		sampler: NewSampler(weights, rand.NewSource(0)),
		clientPacks: []*clientPack{
			{client: newNetmapMock(names[0], true), healthy: true, address: "address0"},
			{client: newNetmapMock(names[1], false), healthy: true, address: "address1"}},
	}
	p := &Pool{
		innerPools: []*innerPool{inner},
		cache:      cache,
		key:        newPrivateKey(t),
	}

	// check getting first node connection before rebalance happened
	connection0, _, err := p.Connection()
	require.NoError(t, err)
	mock0 := connection0.(*clientMock)
	require.Equal(t, names[0], mock0.name)

	updateInnerNodesHealth(context.TODO(), p, 0, options, buffer)

	connection1, _, err := p.Connection()
	require.NoError(t, err)
	mock1 := connection1.(*clientMock)
	require.Equal(t, names[1], mock1.name)

	// enabled first node again
	inner.lock.Lock()
	inner.clientPacks[0].client = newNetmapMock(names[0], false)
	inner.lock.Unlock()

	updateInnerNodesHealth(context.TODO(), p, 0, options, buffer)
	inner.sampler = NewSampler(weights, rand.NewSource(0))

	connection0, _, err = p.Connection()
	require.NoError(t, err)
	mock0 = connection0.(*clientMock)
	require.Equal(t, names[0], mock0.name)
}

func TestHealthyNoReweight(t *testing.T) {
	var (
		weights = []float64{0.9, 0.1}
		names   = []string{"node0", "node1"}
		options = &BuilderOptions{nodesParams: []*NodesParam{{weights: weights}}}
		buffer  = make([]float64, len(weights))
	)

	sampler := NewSampler(weights, rand.NewSource(0))
	inner := &innerPool{
		sampler: sampler,
		clientPacks: []*clientPack{
			{client: newNetmapMock(names[0], false), healthy: true},
			{client: newNetmapMock(names[1], false), healthy: true}},
	}
	p := &Pool{
		innerPools: []*innerPool{inner},
	}

	updateInnerNodesHealth(context.TODO(), p, 0, options, buffer)

	inner.lock.RLock()
	defer inner.lock.RUnlock()
	require.Equal(t, inner.sampler, sampler)
}