package metrics

import (
	"context"
	"encoding/binary"
	"sync"
	"testing"
	"time"

	"github.com/nspcc-dev/neofs-api-go/object"
	"github.com/nspcc-dev/neofs-api-go/refs"
	meta2 "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/meta"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
)

type (
	fakeCounter   int
	fakeIterator  string
	fakeMetaStore []*object.Object
)

var (
	_ ObjectCounter  = (*fakeCounter)(nil)
	_ meta2.Iterator = (*fakeIterator)(nil)
)

func (f fakeCounter) ObjectsCount() (uint64, error) {
	return uint64(f), nil
}

func (f fakeIterator) Iterate(_ meta2.IterateFunc) error {
	if f == "" {
		return nil
	}

	return errors.New(string(f))
}

func (f fakeMetaStore) Iterate(cb meta2.IterateFunc) error {
	if cb == nil {
		return nil
	}

	for i := range f {
		if err := cb(f[i]); err != nil {
			return err
		}
	}

	return nil
}

func TestCollector(t *testing.T) {
	buck := &fakeBucket{items: make(map[uint64]int)}

	t.Run("check errors", func(t *testing.T) {
		t.Run("empty logger", func(t *testing.T) {
			svc, err := New(Params{MetricsStore: buck})
			require.Nil(t, svc)
			require.EqualError(t, err, errEmptyLogger.Error())
		})

		t.Run("empty metrics store", func(t *testing.T) {
			svc, err := New(Params{Logger: zap.L()})
			require.Nil(t, svc)
			require.EqualError(t, err, errEmptyMetricsStore.Error())
		})
	})

	svc, err := New(Params{
		Logger:       zap.L(),
		MetricsStore: buck,
		Options: []string{
			"/Location:Europe/Country:Russia/City:Moscow",
			"/Some:Another/Key:Value",
		},
	})

	require.NoError(t, err)
	require.NotNil(t, svc)

	coll, ok := svc.(*collector)
	require.True(t, ok)
	require.NotNil(t, coll)

	t.Run("check start", func(t *testing.T) {
		coll.interval = time.Second

		t.Run("stop by context", func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
			wg := new(sync.WaitGroup)
			wg.Add(1)

			counter.Store(-1)

			go func() {
				svc.Start(ctx)
				wg.Done()
			}()

			cancel()
			wg.Wait()

			require.Equal(t, float64(-1), counter.Load())
		})

		t.Run("should fail on empty counter", func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
			wg := new(sync.WaitGroup)
			wg.Add(1)

			counter.Store(0)

			go func() {
				svc.Start(ctx)
				wg.Done()
			}()

			time.Sleep(2 * time.Second)
			cancel()
			wg.Wait()

			require.Equal(t, float64(0), counter.Load())
		})

		t.Run("should success on fakeCounter", func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
			wg := new(sync.WaitGroup)
			wg.Add(1)

			coll.SetCounter(fakeCounter(8))
			counter.Store(0)

			go func() {
				svc.Start(ctx)
				wg.Done()
			}()

			time.Sleep(2 * time.Second)
			cancel()
			wg.Wait()

			require.Equal(t, float64(8), counter.Load())
		})
	})

	t.Run("iterator", func(t *testing.T) {
		{
			coll.SetIterator(nil)
			require.Nil(t, coll.metas.iter)
			require.EqualError(t, coll.metas.Iterate(nil), errEmptyMetaStore.Error())
		}

		{
			iter := fakeIterator("")
			coll.SetIterator(iter)
			require.Equal(t, iter, coll.metas.iter)
			require.NoError(t, coll.metas.Iterate(nil))
		}

		{
			iter := fakeIterator("test")
			coll.SetIterator(iter)
			require.Equal(t, iter, coll.metas.iter)
			require.EqualError(t, coll.metas.Iterate(nil), string(iter))
		}
	})

	t.Run("add-rem space", func(t *testing.T) {
		cid := refs.CID{1, 2, 3, 4, 5}
		buf := make([]byte, 8)
		key := keyFromBytes(cid.Bytes())

		zero := make([]byte, 8)
		size := uint64(100)

		binary.BigEndian.PutUint64(buf, size)

		{
			coll.UpdateContainer(cid, size, AddSpace)
			require.Len(t, coll.sizes.items, 1)
			require.Len(t, buck.items, 1)
			require.Contains(t, buck.items, key)
			require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: buf})
		}

		{
			coll.UpdateContainer(cid, size, RemSpace)
			require.Len(t, coll.sizes.items, 1)
			require.Len(t, buck.items, 1)
			require.Contains(t, buck.items, key)
			require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: zero})
		}

		{
			coll.UpdateContainer(cid, size, RemSpace)
			require.Len(t, coll.sizes.items, 1)
			require.Len(t, buck.items, 1)
			require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: zero})
		}
	})

	t.Run("add-rem multi thread", func(t *testing.T) {
		wg := new(sync.WaitGroup)
		wg.Add(10)

		size := uint64(100)
		zero := make([]byte, 8)

		// reset
		coll.UpdateSpaceUsage()

		for i := 0; i < 10; i++ {
			cid := refs.CID{1, 2, 3, 4, byte(i)}
			coll.UpdateContainer(cid, size, AddSpace)

			go func() {
				coll.UpdateContainer(cid, size, RemSpace)
				wg.Done()
			}()
		}

		wg.Wait()

		require.Len(t, coll.sizes.items, 10)
		require.Len(t, buck.items, 10)

		for i := 0; i < 10; i++ {
			cid := refs.CID{1, 2, 3, 4, byte(i)}
			require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: zero})
		}
	})

	t.Run("reset buckets", func(t *testing.T) {
		coll.UpdateSpaceUsage()
		require.Len(t, coll.sizes.items, 0)
		require.Len(t, buck.items, 0)
	})

	t.Run("reset from metaStore", func(t *testing.T) {
		cid := refs.CID{1, 2, 3, 4, 5}
		buf := make([]byte, 8)
		key := keyFromBytes(cid.Bytes())
		size := uint64(100)
		binary.BigEndian.PutUint64(buf, size)

		iter := fakeMetaStore{
			{
				SystemHeader: object.SystemHeader{
					PayloadLength: size,
					CID:           cid,
				},
			},

			{
				Headers: []object.Header{
					{
						Value: &object.Header_Tombstone{Tombstone: &object.Tombstone{}},
					},
				},
			},
		}

		coll.SetIterator(iter)

		coll.UpdateSpaceUsage()
		require.Len(t, coll.sizes.items, 1)
		require.Len(t, buck.items, 1)

		require.Contains(t, buck.items, key)
		require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: buf})
	})
}