package shard

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

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/blobovniczatree"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/fstree"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/internal/testutil"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/writecache"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	"github.com/stretchr/testify/require"
)

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

	t.Run("with write cache", func(t *testing.T) {
		t.Parallel()
		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)})
	}

	wcOpts := []writecache.Option{
		writecache.WithMaxObjectSize(writeCacheMaxSize),
	}

	sh := newCustomShard(t, hasWriteCache, shardOptions{
		wcOpts: wcOpts,
		bsOpts: []blobstor.Option{
			blobstor.WithStorages([]blobstor.SubStorage{
				{
					Storage: blobovniczatree.NewBlobovniczaTree(
						context.Background(),
						blobovniczatree.WithLogger(test.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 func() { require.NoError(t, sh.Close()) }()

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			obj := testutil.GenerateObject()
			testutil.AddAttribute(obj, "foo", "bar")
			testutil.AddPayload(obj, tc.payloadSize)

			addr := object.AddressOf(obj)
			payload := bytes.Clone(obj.Payload())

			var putPrm PutPrm
			putPrm.SetObject(obj)

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

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

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