package shard_test

import (
	"math"
	"path/filepath"
	"testing"

	"github.com/nspcc-dev/neo-go/pkg/util/slice"
	"github.com/nspcc-dev/neofs-node/pkg/core/object"
	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor/blobovniczatree"
	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor/fstree"
	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/writecache"
	apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
	objectSDK "github.com/nspcc-dev/neofs-sdk-go/object"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
)

func TestShard_GetRange(t *testing.T) {
	t.Run("without write cache", func(t *testing.T) {
		testShardGetRange(t, false)
	})

	t.Run("with write cache", func(t *testing.T) {
		testShardGetRange(t, true)
	})
}

func testShardGetRange(t *testing.T, hasWriteCache bool) {
	type testCase struct {
		hasErr      bool
		name        string
		payloadSize int
		rng         *objectSDK.Range
	}

	const (
		writeCacheMaxSize = 1024
		smallObjectSize   = 2048
	)

	newRange := func(off, ln uint64) *objectSDK.Range {
		rng := objectSDK.NewRange()
		rng.SetOffset(off)
		rng.SetLength(ln)
		return rng
	}

	testCases := []testCase{
		{false, "small object, good", 1024, newRange(11, 123)},
		{true, "small object, out of range, big len", 1024, newRange(10, 1020)},
		{true, "small object, out of range, big offset", 1024, newRange(1025, math.MaxUint64-10)},
		{false, "big object, good", 2048, newRange(11, 123)},
		{true, "big object, out of range, big len", 2048, newRange(100, 2000)},
		{true, "big object, out of range, big offset", 2048, newRange(2048, math.MaxUint64-10)},
	}

	if hasWriteCache {
		testCases = append(testCases,
			testCase{false, "object in write-cache, good", 100, newRange(2, 18)},
			testCase{true, "object in write-cache, out of range, big len", 100, newRange(4, 99)},
			testCase{true, "object in write-cache, out of range, big offset", 100, newRange(101, math.MaxUint64-10)})
	}

	sh := newCustomShard(t, t.TempDir(), hasWriteCache,
		[]writecache.Option{writecache.WithMaxObjectSize(writeCacheMaxSize)},
		[]blobstor.Option{blobstor.WithStorages([]blobstor.SubStorage{
			{
				Storage: blobovniczatree.NewBlobovniczaTree(
					blobovniczatree.WithLogger(zaptest.NewLogger(t)),
					blobovniczatree.WithRootPath(filepath.Join(t.TempDir(), "blob", "blobovnicza")),
					blobovniczatree.WithBlobovniczaShallowDepth(1),
					blobovniczatree.WithBlobovniczaShallowWidth(1)),
				Policy: func(_ *objectSDK.Object, data []byte) bool {
					return len(data) <= smallObjectSize
				},
			},
			{
				Storage: fstree.New(
					fstree.WithPath(filepath.Join(t.TempDir(), "blob"))),
			},
		})})
	defer releaseShard(sh, t)

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			obj := generateObject(t)
			addAttribute(obj, "foo", "bar")
			addPayload(obj, tc.payloadSize)

			addr := object.AddressOf(obj)
			payload := slice.Copy(obj.Payload())

			var putPrm shard.PutPrm
			putPrm.SetObject(obj)

			_, err := sh.Put(putPrm)
			require.NoError(t, err)

			var rngPrm shard.RngPrm
			rngPrm.SetAddress(addr)
			rngPrm.SetRange(tc.rng.GetOffset(), tc.rng.GetLength())

			res, err := sh.GetRange(rngPrm)
			if tc.hasErr {
				require.ErrorAs(t, err, &apistatus.ObjectOutOfRange{})
			} else {
				require.Equal(t,
					payload[tc.rng.GetOffset():tc.rng.GetOffset()+tc.rng.GetLength()],
					res.Object().Payload())
			}
		})
	}
}