package writer

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"errors"
	"fmt"
	"strconv"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/client"
	netmapcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/util"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object_manager/placement"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
	rawclient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/rpc/client"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
	apiclient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
	usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
	"git.frostfs.info/TrueCloudLab/tzhash/tz"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/panjf2000/ants/v2"
	"github.com/stretchr/testify/require"
)

type testPlacementBuilder struct {
	vectors [][]netmap.NodeInfo
}

func (p *testPlacementBuilder) BuildPlacement(_ cid.ID, _ *oid.ID, _ netmap.PlacementPolicy) (
	[][]netmap.NodeInfo, error,
) {
	arr := make([]netmap.NodeInfo, len(p.vectors[0]))
	copy(arr, p.vectors[0])
	return [][]netmap.NodeInfo{arr}, nil
}

type nmKeys struct{}

func (nmKeys) IsLocalKey(_ []byte) bool {
	return false
}

type clientConstructor struct {
	vectors [][]netmap.NodeInfo
}

func (c clientConstructor) Get(info client.NodeInfo) (client.MultiAddressClient, error) {
	if bytes.Equal(info.PublicKey(), c.vectors[0][0].PublicKey()) ||
		bytes.Equal(info.PublicKey(), c.vectors[0][1].PublicKey()) {
		return multiAddressClient{err: errors.New("node unavailable")}, nil
	}
	return multiAddressClient{}, nil
}

type multiAddressClient struct {
	client.MultiAddressClient
	err error
}

func (c multiAddressClient) ObjectPutSingle(_ context.Context, _ apiclient.PrmObjectPutSingle) (*apiclient.ResObjectPutSingle, error) {
	if c.err != nil {
		return nil, c.err
	}
	return &apiclient.ResObjectPutSingle{}, nil
}

func (c multiAddressClient) ReportError(error) {
}

func (multiAddressClient) RawForAddress(context.Context, network.Address, func(cli *rawclient.Client) error) error {
	return nil
}

func TestECWriter(t *testing.T) {
	// Create container with policy EC 1.1
	cnr := container.Container{}
	p1 := netmap.PlacementPolicy{}
	p1.SetContainerBackupFactor(1)
	x1 := netmap.ReplicaDescriptor{}
	x1.SetECDataCount(1)
	x1.SetECParityCount(1)
	p1.AddReplicas(x1)
	cnr.SetPlacementPolicy(p1)
	cnr.SetAttribute("cnr", "cnr1")

	cid := cidtest.ID()

	// Create 4 nodes, 2 nodes for chunks,
	// 2 nodes for the case when the first two will fail.
	ns, _ := testNodeMatrix(t, []int{4})

	data := make([]byte, 100)
	_, _ = rand.Read(data)
	ver := version.Current()

	var csum checksum.Checksum
	csum.SetSHA256(sha256.Sum256(data))

	var csumTZ checksum.Checksum
	csumTZ.SetTillichZemor(tz.Sum(csum.Value()))

	obj := objectSDK.New()
	obj.SetID(oidtest.ID())
	obj.SetOwnerID(usertest.ID())
	obj.SetContainerID(cid)
	obj.SetVersion(&ver)
	obj.SetPayload(data)
	obj.SetPayloadSize(uint64(len(data)))
	obj.SetPayloadChecksum(csum)
	obj.SetPayloadHomomorphicHash(csumTZ)

	// Builder return nodes without sort by hrw
	builder := &testPlacementBuilder{
		vectors: ns,
	}

	ownerKey, err := keys.NewPrivateKey()
	require.NoError(t, err)
	nodeKey, err := keys.NewPrivateKey()
	require.NoError(t, err)

	pool, err := ants.NewPool(4, ants.WithNonblocking(true))
	require.NoError(t, err)

	log, err := logger.NewLogger(nil)
	require.NoError(t, err)

	var n nmKeys
	ecw := ECWriter{
		Config: &Config{
			NetmapKeys:        n,
			RemotePool:        pool,
			Logger:            log,
			ClientConstructor: clientConstructor{vectors: ns},
			KeyStorage:        util.NewKeyStorage(&nodeKey.PrivateKey, nil, nil),
		},
		PlacementOpts: append(
			[]placement.Option{placement.UseBuilder(builder), placement.ForContainer(cnr)},
			placement.WithCopyNumbers(nil)), // copies number ignored for EC
		Container:       cnr,
		Key:             &ownerKey.PrivateKey,
		Relay:           nil,
		ObjectMetaValid: true,
	}

	err = ecw.WriteObject(context.Background(), obj)
	require.NoError(t, err)
}

func testNodeMatrix(t testing.TB, dim []int) ([][]netmap.NodeInfo, [][]string) {
	mNodes := make([][]netmap.NodeInfo, len(dim))
	mAddr := make([][]string, len(dim))

	for i := range dim {
		ns := make([]netmap.NodeInfo, dim[i])
		as := make([]string, dim[i])

		for j := range dim[i] {
			a := fmt.Sprintf("/ip4/192.168.0.%s/tcp/%s",
				strconv.Itoa(i),
				strconv.Itoa(60000+j),
			)

			var ni netmap.NodeInfo
			ni.SetNetworkEndpoints(a)
			ni.SetPublicKey([]byte(a))

			var na network.AddressGroup

			err := na.FromIterator(netmapcore.Node(ni))
			require.NoError(t, err)

			as[j] = network.StringifyGroup(na)

			ns[j] = ni
		}

		mNodes[i] = ns
		mAddr[i] = as
	}

	return mNodes, mAddr
}