package benchmark

import (
	"context"
	"fmt"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/teststore"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/internal/testutil"
	meta "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/metabase"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard/mode"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/writecache"
	"github.com/stretchr/testify/require"
)

func BenchmarkWritecacheSeq(b *testing.B) {
	const payloadSize = 8 << 10
	b.Run("bbolt_seq", func(b *testing.B) {
		benchmarkPutSeq(b, newCache(b), payloadSize)
	})
}

func BenchmarkWritecachePar(b *testing.B) {
	const payloadSize = 8 << 10
	b.Run("bbolt_par", func(b *testing.B) {
		benchmarkPutPar(b, newCache(b), payloadSize)
	})
}

func BenchmarkWriteAfterDelete(b *testing.B) {
	const payloadSize = 32 << 10
	const parallel = 25

	cache := newCache(b)
	benchmarkPutPrepare(b, cache)
	b.Run(fmt.Sprintf("%dB_before", payloadSize), func(b *testing.B) {
		b.SetParallelism(parallel)
		benchmarkRunPar(b, cache, payloadSize)
	})
	require.NoError(b, cache.Flush(context.Background(), false, false))
	b.Run(fmt.Sprintf("%dB_after", payloadSize), func(b *testing.B) {
		b.SetParallelism(parallel)
		benchmarkRunPar(b, cache, payloadSize)
	})
	require.NoError(b, cache.Close())
}

func benchmarkPutSeq(b *testing.B, cache writecache.Cache, size uint64) {
	benchmarkPutPrepare(b, cache)
	defer func() { require.NoError(b, cache.Close()) }()

	ctx := context.Background()
	objGen := testutil.RandObjGenerator{ObjSize: size}

	b.ResetTimer()
	for range b.N {
		obj := objGen.Next()
		rawData, err := obj.Marshal()
		require.NoError(b, err, "marshaling object")
		prm := common.PutPrm{
			Address: testutil.AddressFromObject(b, obj),
			Object:  obj,
			RawData: rawData,
		}
		if _, err := cache.Put(ctx, prm); err != nil {
			b.Fatalf("putting: %v", err)
		}
	}
}

func benchmarkPutPar(b *testing.B, cache writecache.Cache, size uint64) {
	benchmarkPutPrepare(b, cache)
	defer func() { require.NoError(b, cache.Close()) }()

	benchmarkRunPar(b, cache, size)
}

func benchmarkRunPar(b *testing.B, cache writecache.Cache, size uint64) {
	ctx := context.Background()

	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		objGen := testutil.RandObjGenerator{ObjSize: size}
		for pb.Next() {
			obj := objGen.Next()
			rawData, err := obj.Marshal()
			require.NoError(b, err, "marshaling object")
			prm := common.PutPrm{
				Address: testutil.AddressFromObject(b, obj),
				Object:  obj,
				RawData: rawData,
			}
			if _, err := cache.Put(ctx, prm); err != nil {
				b.Fatalf("putting: %v", err)
			}
		}
	})
}

func benchmarkPutPrepare(b *testing.B, cache writecache.Cache) {
	require.NoError(b, cache.Open(context.Background(), mode.ReadWrite), "opening")
	require.NoError(b, cache.Init(), "initializing")
}

type testMetabase struct{}

func (testMetabase) UpdateStorageID(context.Context, meta.UpdateStorageIDPrm) (meta.UpdateStorageIDRes, error) {
	return meta.UpdateStorageIDRes{}, nil
}

func newCache(b *testing.B) writecache.Cache {
	bs := teststore.New(
		teststore.WithPut(func(pp common.PutPrm) (common.PutRes, error) { return common.PutRes{}, nil }),
	)
	return writecache.New(
		writecache.WithPath(b.TempDir()),
		writecache.WithBlobstor(bs),
		writecache.WithMetabase(testMetabase{}),
		writecache.WithMaxCacheSize(256<<30),
	)
}