package blobstor

import (
	"context"
	"encoding/binary"
	"errors"
	"os"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/memstore"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/teststore"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard/mode"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
	"github.com/stretchr/testify/require"
)

func TestIterateObjects(t *testing.T) {
	p := t.Name()

	const smalSz = 50

	// create BlobStor instance
	blobStor := New(
		WithStorages(defaultStorages(p, smalSz)),
		WithCompressObjects(true),
	)

	defer os.RemoveAll(p)

	// open Blobstor
	require.NoError(t, blobStor.Open(context.Background(), mode.ReadWrite))

	// initialize Blobstor
	require.NoError(t, blobStor.Init())

	defer blobStor.Close()

	const objNum = 5

	type addrData struct {
		big  bool
		addr oid.Address
		data []byte
	}

	mObjs := make(map[string]addrData)

	for i := range uint64(objNum) {
		sz := smalSz

		big := i < objNum/2
		if big {
			sz++
		}

		data := make([]byte, sz)
		binary.BigEndian.PutUint64(data, i)

		addr := oidtest.Address()

		mObjs[string(data)] = addrData{
			big:  big,
			addr: addr,
			data: data,
		}
	}

	for _, v := range mObjs {
		_, err := blobStor.Put(context.Background(), common.PutPrm{Address: v.addr, RawData: v.data})
		require.NoError(t, err)
	}

	err := IterateBinaryObjects(context.Background(), blobStor, func(addr oid.Address, data []byte, descriptor []byte) error {
		v, ok := mObjs[string(data)]
		require.True(t, ok)

		require.Equal(t, v.data, data)

		if v.big {
			require.True(t, descriptor != nil && len(descriptor) == 0)
		} else {
			require.NotEmpty(t, descriptor)
		}

		delete(mObjs, string(data))

		return nil
	})
	require.NoError(t, err)
	require.Empty(t, mObjs)
}

func TestIterate_IgnoreErrors(t *testing.T) {
	ctx := context.Background()

	myErr := errors.New("unique error")
	nopIter := func(common.IteratePrm) (common.IterateRes, error) { return common.IterateRes{}, nil }
	panicIter := func(common.IteratePrm) (common.IterateRes, error) { panic("unreachable") }
	errIter := func(common.IteratePrm) (common.IterateRes, error) { return common.IterateRes{}, myErr }

	var s1iter, s2iter func(common.IteratePrm) (common.IterateRes, error)
	st1 := teststore.New(
		teststore.WithSubstorage(memstore.New()),
		teststore.WithIterate(func(prm common.IteratePrm) (common.IterateRes, error) {
			return s1iter(prm)
		}))
	st2 := teststore.New(
		teststore.WithSubstorage(memstore.New()),
		teststore.WithIterate(func(prm common.IteratePrm) (common.IterateRes, error) {
			return s2iter(prm)
		}))

	bsOpts := []Option{WithStorages([]SubStorage{
		{Storage: st1},
		{Storage: st2},
	})}
	bs := New(bsOpts...)
	require.NoError(t, bs.Open(ctx, mode.ReadWrite))
	require.NoError(t, bs.Init())

	nopHandler := func(e common.IterationElement) error {
		return nil
	}

	t.Run("no errors", func(t *testing.T) {
		s1iter = nopIter
		s2iter = nopIter
		_, err := bs.Iterate(ctx, common.IteratePrm{Handler: nopHandler})
		require.NoError(t, err)
	})
	t.Run("error in the first sub storage, the second one is not iterated over", func(t *testing.T) {
		s1iter = errIter
		s2iter = panicIter
		_, err := bs.Iterate(ctx, common.IteratePrm{Handler: nopHandler})
		require.ErrorIs(t, err, myErr)
	})

	t.Run("ignore errors, storage 1", func(t *testing.T) {
		s1iter = errIter
		s2iter = nopIter
		_, err := bs.Iterate(ctx, common.IteratePrm{IgnoreErrors: true, Handler: nopHandler})
		require.NoError(t, err)
	})
	t.Run("ignore errors, storage 2", func(t *testing.T) {
		s1iter = nopIter
		s2iter = errIter
		_, err := bs.Iterate(ctx, common.IteratePrm{IgnoreErrors: true, Handler: nopHandler})
		require.NoError(t, err)
	})
}