package netmap

import (
	"fmt"
	"math/rand"
	"sort"
	"strconv"
	"testing"

	"github.com/nspcc-dev/hrw"
	"github.com/nspcc-dev/neofs-api-go/v2/netmap"
	testv2 "github.com/nspcc-dev/neofs-api-go/v2/netmap/test"
	"github.com/stretchr/testify/require"
)

func BenchmarkHRWSort(b *testing.B) {
	const netmapSize = 1000

	nodes := make([]Nodes, netmapSize)
	weights := make([]float64, netmapSize)
	for i := range nodes {
		nodes[i] = Nodes{{
			ID:       rand.Uint64(),
			Index:    i,
			Capacity: 100,
			Price:    1,
			AttrMap:  nil,
		}}
		weights[i] = float64(rand.Uint32()%10) / 10.0
	}

	pivot := rand.Uint64()
	b.Run("sort by index, no weight", func(b *testing.B) {
		realNodes := make([]Nodes, netmapSize)
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			b.StopTimer()
			copy(realNodes, nodes)
			b.StartTimer()

			hrw.SortSliceByIndex(realNodes, pivot)
		}
	})
	b.Run("sort by value, no weight", func(b *testing.B) {
		realNodes := make([]Nodes, netmapSize)
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			b.StopTimer()
			copy(realNodes, nodes)
			b.StartTimer()

			hrw.SortSliceByValue(realNodes, pivot)
		}
	})
	b.Run("only sort by index", func(b *testing.B) {
		realNodes := make([]Nodes, netmapSize)
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			b.StopTimer()
			copy(realNodes, nodes)
			b.StartTimer()

			hrw.SortSliceByWeightIndex(realNodes, weights, pivot)
		}
	})
	b.Run("sort by value", func(b *testing.B) {
		realNodes := make([]Nodes, netmapSize)
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			b.StopTimer()
			copy(realNodes, nodes)
			b.StartTimer()

			hrw.SortSliceByWeightValue(realNodes, weights, pivot)
		}
	})
	b.Run("sort by ID, then by index (deterministic)", func(b *testing.B) {
		realNodes := make([]Nodes, netmapSize)
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			b.StopTimer()
			copy(realNodes, nodes)
			b.StartTimer()

			sort.Slice(nodes, func(i, j int) bool {
				return nodes[i][0].ID < nodes[j][0].ID
			})
			hrw.SortSliceByWeightIndex(realNodes, weights, pivot)
		}
	})
}

func BenchmarkPolicyHRWType(b *testing.B) {
	const netmapSize = 100

	p := newPlacementPolicy(1,
		[]*Replica{
			newReplica(1, "loc1"),
			newReplica(1, "loc2")},
		[]*Selector{
			newSelector("loc1", "Location", ClauseSame, 1, "loc1"),
			newSelector("loc2", "Location", ClauseSame, 1, "loc2")},
		[]*Filter{
			newFilter("loc1", "Location", "Shanghai", OpEQ),
			newFilter("loc2", "Location", "Shanghai", OpNE),
		})

	nodes := make([]NodeInfo, netmapSize)
	for i := range nodes {
		var loc string
		switch i % 20 {
		case 0:
			loc = "Shanghai"
		default:
			loc = strconv.Itoa(i % 20)
		}

		// Having the same price and capacity ensures equal weights for all nodes.
		// This way placement is more dependent on the initial order.
		nodes[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10")
		pub := make([]byte, 33)
		pub[0] = byte(i)
		nodes[i].SetPublicKey(pub)
	}

	nm, err := NewNetmap(NodesFromInfo(nodes))
	require.NoError(b, err)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := nm.GetContainerNodes(p, []byte{1})
		if err != nil {
			b.Fatal()
		}
	}
}

func TestPlacementPolicy_DeterministicOrder(t *testing.T) {
	const netmapSize = 100

	p := newPlacementPolicy(1,
		[]*Replica{
			newReplica(1, "loc1"),
			newReplica(1, "loc2")},
		[]*Selector{
			newSelector("loc1", "Location", ClauseSame, 1, "loc1"),
			newSelector("loc2", "Location", ClauseSame, 1, "loc2")},
		[]*Filter{
			newFilter("loc1", "Location", "Shanghai", OpEQ),
			newFilter("loc2", "Location", "Shanghai", OpNE),
		})

	nodes := make([]NodeInfo, netmapSize)
	for i := range nodes {
		var loc string
		switch i % 20 {
		case 0:
			loc = "Shanghai"
		default:
			loc = strconv.Itoa(i % 20)
		}

		// Having the same price and capacity ensures equal weights for all nodes.
		// This way placement is more dependent on the initial order.
		nodes[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10")
		pub := make([]byte, 33)
		pub[0] = byte(i)
		nodes[i].SetPublicKey(pub)
	}

	nm, err := NewNetmap(NodesFromInfo(nodes))
	require.NoError(t, err)
	getIndices := func(t *testing.T) (int, int) {
		v, err := nm.GetContainerNodes(p, []byte{1})
		require.NoError(t, err)
		ns := v.Flatten()
		require.Equal(t, 2, len(ns))
		return ns[0].Index, ns[1].Index
	}

	a, b := getIndices(t)
	for i := 0; i < 10; i++ {
		x, y := getIndices(t)
		require.Equal(t, a, x)
		require.Equal(t, b, y)
	}
}

func TestPlacementPolicy_ProcessSelectors(t *testing.T) {
	p := newPlacementPolicy(2, nil,
		[]*Selector{
			newSelector("SameRU", "City", ClauseSame, 2, "FromRU"),
			newSelector("DistinctRU", "City", ClauseDistinct, 2, "FromRU"),
			newSelector("Good", "Country", ClauseDistinct, 2, "Good"),
			newSelector("Main", "Country", ClauseDistinct, 3, "*"),
		},
		[]*Filter{
			newFilter("FromRU", "Country", "Russia", OpEQ),
			newFilter("Good", "Rating", "4", OpGE),
		})
	nodes := []NodeInfo{
		nodeInfoFromAttributes("Country", "Russia", "Rating", "1", "City", "SPB"),
		nodeInfoFromAttributes("Country", "Germany", "Rating", "5", "City", "Berlin"),
		nodeInfoFromAttributes("Country", "Russia", "Rating", "6", "City", "Moscow"),
		nodeInfoFromAttributes("Country", "France", "Rating", "4", "City", "Paris"),
		nodeInfoFromAttributes("Country", "France", "Rating", "1", "City", "Lyon"),
		nodeInfoFromAttributes("Country", "Russia", "Rating", "5", "City", "SPB"),
		nodeInfoFromAttributes("Country", "Russia", "Rating", "7", "City", "Moscow"),
		nodeInfoFromAttributes("Country", "Germany", "Rating", "3", "City", "Darmstadt"),
		nodeInfoFromAttributes("Country", "Germany", "Rating", "7", "City", "Frankfurt"),
		nodeInfoFromAttributes("Country", "Russia", "Rating", "9", "City", "SPB"),
		nodeInfoFromAttributes("Country", "Russia", "Rating", "9", "City", "SPB"),
	}

	nm, err := NewNetmap(NodesFromInfo(nodes))
	require.NoError(t, err)
	c := NewContext(nm)
	c.setCBF(p.ContainerBackupFactor())
	require.NoError(t, c.processFilters(p))
	require.NoError(t, c.processSelectors(p))

	for _, s := range p.Selectors() {
		sel := c.Selections[s.Name()]
		s := c.Selectors[s.Name()]
		bucketCount, nodesInBucket := GetNodesCount(p, s)
		nodesInBucket *= int(c.cbf)
		targ := fmt.Sprintf("selector '%s'", s.Name())
		require.Equal(t, bucketCount, len(sel), targ)
		for _, res := range sel {
			require.Equal(t, nodesInBucket, len(res), targ)
			for j := range res {
				require.True(t, c.applyFilter(s.Filter(), res[j]), targ)
			}
		}
	}
}

func testSelector() *Selector {
	s := new(Selector)
	s.SetName("name")
	s.SetCount(3)
	s.SetFilter("filter")
	s.SetAttribute("attribute")
	s.SetClause(ClauseDistinct)

	return s
}

func TestSelector_Name(t *testing.T) {
	s := NewSelector()
	name := "some name"

	s.SetName(name)

	require.Equal(t, name, s.Name())
}

func TestSelector_Count(t *testing.T) {
	s := NewSelector()
	c := uint32(3)

	s.SetCount(c)

	require.Equal(t, c, s.Count())
}

func TestSelector_Clause(t *testing.T) {
	s := NewSelector()
	c := ClauseSame

	s.SetClause(c)

	require.Equal(t, c, s.Clause())
}

func TestSelector_Attribute(t *testing.T) {
	s := NewSelector()
	a := "some attribute"

	s.SetAttribute(a)

	require.Equal(t, a, s.Attribute())
}

func TestSelector_Filter(t *testing.T) {
	s := NewSelector()
	f := "some filter"

	s.SetFilter(f)

	require.Equal(t, f, s.Filter())
}

func TestSelectorEncoding(t *testing.T) {
	s := newSelector("name", "atte", ClauseSame, 1, "filter")

	t.Run("binary", func(t *testing.T) {
		data, err := s.Marshal()
		require.NoError(t, err)

		s2 := NewSelector()
		require.NoError(t, s2.Unmarshal(data))

		require.Equal(t, s, s2)
	})

	t.Run("json", func(t *testing.T) {
		data, err := s.MarshalJSON()
		require.NoError(t, err)

		s2 := NewSelector()
		require.NoError(t, s2.UnmarshalJSON(data))

		require.Equal(t, s, s2)
	})
}

func TestSelector_ToV2(t *testing.T) {
	t.Run("nil", func(t *testing.T) {
		var x *Selector

		require.Nil(t, x.ToV2())
	})
}

func TestNewSelectorFromV2(t *testing.T) {
	t.Run("from nil", func(t *testing.T) {
		var x *netmap.Selector

		require.Nil(t, NewSelectorFromV2(x))
	})

	t.Run("from non-nil", func(t *testing.T) {
		sV2 := testv2.GenerateSelector(false)

		s := NewSelectorFromV2(sV2)

		require.Equal(t, sV2, s.ToV2())
	})
}

func TestNewSelector(t *testing.T) {
	t.Run("default values", func(t *testing.T) {
		s := NewSelector()

		// check initial values
		require.Zero(t, s.Count())
		require.Equal(t, ClauseUnspecified, s.Clause())
		require.Empty(t, s.Attribute())
		require.Empty(t, s.Name())
		require.Empty(t, s.Filter())

		// convert to v2 message
		sV2 := s.ToV2()

		require.Zero(t, sV2.GetCount())
		require.Equal(t, netmap.UnspecifiedClause, sV2.GetClause())
		require.Empty(t, sV2.GetAttribute())
		require.Empty(t, sV2.GetName())
		require.Empty(t, sV2.GetFilter())
	})
}