package netmap

import (
	"errors"
	"strconv"
	"testing"

	"github.com/nspcc-dev/neofs-api-go/v2/netmap"
	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const subnetAttrPrefix = "__NEOFS_SUBNET"

func subnetAttrName(subnet uint32) string {
	return subnetAttrPrefix + "." +
		strconv.FormatUint(uint64(subnet), 10) + ".ENABLED"
}

func TestPlacementPolicy_Subnet(t *testing.T) {
	nodes := []NodeInfo{
		nodeInfoFromAttributes("ID", "0", "City", "Paris"),
		nodeInfoFromAttributes("ID", "1", "City", "Paris"),
		nodeInfoFromAttributes("ID", "2", "City", "London"),
		nodeInfoFromAttributes("ID", "3", "City", "London"),
		nodeInfoFromAttributes("ID", "4", "City", "Toronto"),
		nodeInfoFromAttributes("ID", "5", "City", "Toronto"),
		nodeInfoFromAttributes("ID", "6", "City", "Tokyo"),
		nodeInfoFromAttributes("ID", "7", "City", "Tokyo"),
	}
	var id subnetid.ID
	nodes[0].ExitSubnet(id)

	id.SetNumber(1)
	nodes[2].EnterSubnet(id)
	nodes[4].EnterSubnet(id)

	id.SetNumber(2)
	nodes[5].EnterSubnet(id)
	nodes[6].EnterSubnet(id)
	nodes[7].EnterSubnet(id)

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

	t.Run("select 2 nodes from the default subnet in Paris", func(t *testing.T) {
		p := newPlacementPolicy(0,
			[]*Replica{newReplica(1, "S")},
			[]*Selector{newSelector("S", "City", ClauseSame, 2, "F")},
			[]*Filter{newFilter("F", "City", "Paris", OpEQ)})

		_, err := nm.GetContainerNodes(p, nil)
		require.True(t, errors.Is(err, ErrNotEnoughNodes), "got: %v", err)
	})
	t.Run("select 2 nodes from the default subnet in London", func(t *testing.T) {
		p := newPlacementPolicy(0,
			[]*Replica{newReplica(1, "S")},
			[]*Selector{newSelector("S", "City", ClauseSame, 2, "F")},
			[]*Filter{newFilter("F", "City", "London", OpEQ)})

		v, err := nm.GetContainerNodes(p, nil)
		require.NoError(t, err)

		nodes := v.Flatten()
		require.Equal(t, 2, len(nodes))
		for _, n := range v.Flatten() {
			id := n.Attribute("ID")
			require.Contains(t, []string{"2", "3"}, id)
		}
	})
	t.Run("select 2 nodes from the default subnet in Toronto", func(t *testing.T) {
		p := newPlacementPolicy(0,
			[]*Replica{newReplica(1, "S")},
			[]*Selector{newSelector("S", "City", ClauseSame, 2, "F")},
			[]*Filter{newFilter("F", "City", "Toronto", OpEQ)})

		v, err := nm.GetContainerNodes(p, nil)
		require.NoError(t, err)

		nodes := v.Flatten()
		require.Equal(t, 2, len(nodes))
		for _, n := range v.Flatten() {
			id := n.Attribute("ID")
			require.Contains(t, []string{"4", "5"}, id)
		}
	})
	t.Run("select 3 nodes from the non-default subnet", func(t *testing.T) {
		p := newPlacementPolicy(0,
			[]*Replica{newReplica(3, "")},
			nil, nil)
		p.SetSubnetID(newSubnetID(2))

		v, err := nm.GetContainerNodes(p, nil)
		require.NoError(t, err)

		nodes := v.Flatten()
		require.Equal(t, 3, len(nodes))
		for _, n := range v.Flatten() {
			id := n.Attribute("ID")
			require.Contains(t, []string{"5", "6", "7"}, id)
		}
	})
	t.Run("select nodes from the subnet via filter", func(t *testing.T) {
		p := newPlacementPolicy(0,
			[]*Replica{newReplica(1, "")},
			nil,
			[]*Filter{newFilter(MainFilterName, subnetAttrName(2), "True", OpEQ, nil)})

		_, err := nm.GetContainerNodes(p, nil)
		require.Error(t, err)
	})
}

func TestPlacementPolicy_CBFWithEmptySelector(t *testing.T) {
	nodes := []NodeInfo{
		nodeInfoFromAttributes("ID", "1", "Attr", "Same"),
		nodeInfoFromAttributes("ID", "2", "Attr", "Same"),
		nodeInfoFromAttributes("ID", "3", "Attr", "Same"),
		nodeInfoFromAttributes("ID", "4", "Attr", "Same"),
	}

	p1 := newPlacementPolicy(0,
		[]*Replica{newReplica(2, "")},
		nil, // selectors
		nil, // filters
	)

	p2 := newPlacementPolicy(3,
		[]*Replica{newReplica(2, "")},
		nil, // selectors
		nil, // filters
	)

	p3 := newPlacementPolicy(3,
		[]*Replica{newReplica(2, "X")},
		[]*Selector{newSelector("X", "", ClauseDistinct, 2, "*")},
		nil, // filters
	)

	p4 := newPlacementPolicy(3,
		[]*Replica{newReplica(2, "X")},
		[]*Selector{newSelector("X", "Attr", ClauseSame, 2, "*")},
		nil, // filters
	)

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

	v, err := nm.GetContainerNodes(p1, nil)
	require.NoError(t, err)
	assert.Len(t, v.Flatten(), 4)

	v, err = nm.GetContainerNodes(p2, nil)
	require.NoError(t, err)
	assert.Len(t, v.Flatten(), 4)

	v, err = nm.GetContainerNodes(p3, nil)
	require.NoError(t, err)
	assert.Len(t, v.Flatten(), 4)

	v, err = nm.GetContainerNodes(p4, nil)
	require.NoError(t, err)
	assert.Len(t, v.Flatten(), 4)
}

func TestPlacementPolicyFromV2(t *testing.T) {
	pV2 := new(netmap.PlacementPolicy)

	pV2.SetReplicas([]*netmap.Replica{
		testReplica().ToV2(),
		testReplica().ToV2(),
	})

	pV2.SetContainerBackupFactor(3)

	pV2.SetSelectors([]*netmap.Selector{
		testSelector().ToV2(),
		testSelector().ToV2(),
	})

	pV2.SetFilters([]*netmap.Filter{
		testFilter().ToV2(),
		testFilter().ToV2(),
	})

	p := NewPlacementPolicyFromV2(pV2)

	require.Equal(t, pV2, p.ToV2())
}

func TestPlacementPolicy_Replicas(t *testing.T) {
	p := NewPlacementPolicy()
	rs := []*Replica{testReplica(), testReplica()}

	p.SetReplicas(rs...)

	require.Equal(t, rs, p.Replicas())
}

func TestPlacementPolicy_ContainerBackupFactor(t *testing.T) {
	p := NewPlacementPolicy()
	f := uint32(3)

	p.SetContainerBackupFactor(f)

	require.Equal(t, f, p.ContainerBackupFactor())
}

func TestPlacementPolicy_Selectors(t *testing.T) {
	p := NewPlacementPolicy()
	ss := []*Selector{testSelector(), testSelector()}

	p.SetSelectors(ss...)

	require.Equal(t, ss, p.Selectors())
}

func TestPlacementPolicy_Filters(t *testing.T) {
	p := NewPlacementPolicy()
	fs := []*Filter{testFilter(), testFilter()}

	p.SetFilters(fs...)

	require.Equal(t, fs, p.Filters())
}

func TestPlacementPolicyEncoding(t *testing.T) {
	p := newPlacementPolicy(3, nil, nil, nil)

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

		p2 := NewPlacementPolicy()
		require.NoError(t, p2.Unmarshal(data))

		require.Equal(t, p, p2)
	})

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

		p2 := NewPlacementPolicy()
		require.NoError(t, p2.UnmarshalJSON(data))

		require.Equal(t, p, p2)
	})
}

func TestNewPlacementPolicy(t *testing.T) {
	t.Run("nil", func(t *testing.T) {
		var x *PlacementPolicy

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

	t.Run("default values", func(t *testing.T) {
		pp := NewPlacementPolicy()

		// check initial values
		require.Nil(t, pp.Replicas())
		require.Nil(t, pp.Filters())
		require.Nil(t, pp.Selectors())
		require.Zero(t, pp.ContainerBackupFactor())

		// convert to v2 message
		ppV2 := pp.ToV2()

		require.Nil(t, ppV2.GetReplicas())
		require.Nil(t, ppV2.GetFilters())
		require.Nil(t, ppV2.GetSelectors())
		require.Zero(t, ppV2.GetContainerBackupFactor())
	})
}

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

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