package getsvc

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

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	coreContainer "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/policy"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object_manager/placement"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/erasurecode"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/stretchr/testify/require"
)

type containerStorage struct {
	cnt *container.Container
}

func (cs *containerStorage) Get(cid.ID) (*coreContainer.Container, error) {
	coreCnt := coreContainer.Container{
		Value: *cs.cnt,
	}
	return &coreCnt, nil
}

func (cs *containerStorage) DeletionInfo(cid.ID) (*coreContainer.DelInfo, error) {
	return nil, nil
}

func TestGetRangeEC(t *testing.T) {
	var dataCount uint32 = 3
	var parityCount uint32 = 1
	cnr := container.Container{}
	p := netmap.PlacementPolicy{}
	p.SetContainerBackupFactor(1)
	x := netmap.ReplicaDescriptor{}
	x.SetECDataCount(dataCount)
	x.SetECParityCount(parityCount)
	p.AddReplicas(x)
	cnr.SetPlacementPolicy(p)

	var idCnr cid.ID
	container.CalculateID(&idCnr, cnr)

	ns, as := testNodeMatrix(t, []int{4})

	testGetRange := func(t *testing.T, svc *Service, addr oid.Address, from, to uint64, payload []byte) {
		w := NewSimpleObjectWriter()
		rngPrm := newRngPrm(false, w, from, to-from)
		rngPrm.WithAddress(addr)

		err := svc.GetRange(context.Background(), rngPrm)
		require.NoError(t, err)
		if from == to {
			require.Nil(t, w.Object().Payload())
		} else {
			require.Equal(t, payload[from:to], w.Object().Payload())
		}
	}

	newSvc := func(b *testPlacementBuilder, c *testClientCache) *Service {
		const curEpoch = 13

		return &Service{
			log:          test.NewLogger(t),
			localStorage: newTestStorage(),
			traverserGenerator: &testTraverserGenerator{
				c: cnr,
				b: map[uint64]placement.Builder{
					curEpoch: b,
				},
			},
			epochSource:              testEpochReceiver(curEpoch),
			remoteStorageConstructor: c,
			keyStore:                 &testKeyStorage{},
			containerSource: &containerStorage{
				cnt: &cnr,
			},
		}
	}
	const totalSize = 5
	obj, parts := objectECChain(t, &idCnr, &cnr, totalSize, totalSize)
	require.Len(t, parts, int(dataCount+parityCount))
	require.Len(t, obj.Payload(), totalSize)

	addr := object.AddressOf(obj)
	builder := &testPlacementBuilder{
		vectors: map[string][][]netmap.NodeInfo{
			addr.EncodeToString(): ns,
		},
	}

	clients := map[string]*testClient{}
	for i, part := range parts {
		builder.vectors[object.AddressOf(part).EncodeToString()] = ns

		tc := newTestClient()

		ecInfo := objectSDK.NewECInfo()

		chunk := objectSDK.ECChunk{}
		chunk.Total = uint32(len(parts))
		chunk.Index = uint32(i)
		id, _ := part.ID()
		idv2 := refs.ObjectID{}
		id.WriteToV2(&idv2)
		chunk.ID = idv2

		ecInfo.AddChunk(chunk)
		errECInfo := objectSDK.NewECInfoError(ecInfo)

		tc.addResult(addr, nil, errECInfo)
		tc.addResult(object.AddressOf(part), part, nil)

		clients[as[0][i]] = tc
	}

	svc := newSvc(builder, &testClientCache{
		clients: clients,
	})

	for from := 0; from < totalSize-1; from++ {
		for to := from; to < totalSize; to++ {
			t.Run(fmt.Sprintf("from=%d,to=%d", from, to), func(t *testing.T) {
				testGetRange(t, svc, addr, uint64(from), uint64(to), obj.Payload())
			})
		}
	}
}

func objectECChain(t *testing.T, cnrId *cid.ID, cnr *container.Container, singleSize, totalSize uint64) (*objectSDK.Object, []*objectSDK.Object) {
	pk, err := keys.NewPrivateKey()
	require.NoError(t, err)

	tt := new(testTarget)
	p := transformer.NewPayloadSizeLimiter(transformer.Params{
		Key:            &pk.PrivateKey,
		NextTargetInit: func() transformer.ObjectWriter { return tt },
		NetworkState:   testEpochReceiver(1),
		MaxSize:        singleSize,
	})

	payload := make([]byte, totalSize)
	_, err = rand.Read(payload)
	require.NoError(t, err)

	ver := version.Current()
	hdr := objectSDK.New()
	hdr.SetContainerID(*cnrId)
	hdr.SetType(objectSDK.TypeRegular)
	hdr.SetVersion(&ver)

	ctx := context.Background()
	require.NoError(t, p.WriteHeader(ctx, hdr))

	_, err = p.Write(ctx, payload)
	require.NoError(t, err)

	_, err = p.Close(ctx)
	require.NoError(t, err)

	require.Len(t, tt.objects, 1)

	c, err := erasurecode.NewConstructor(policy.ECDataCount(cnr.PlacementPolicy()), policy.ECParityCount(cnr.PlacementPolicy()))
	require.NoError(t, err)
	parts, err := c.Split(tt.objects[0], &pk.PrivateKey)
	require.NoError(t, err)

	return tt.objects[0], parts
}