// nolint: vetshadow
package lite_test

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/tendermint/tendermint/lite"
	liteErr "github.com/tendermint/tendermint/lite/errors"
)

// missingProvider doesn't store anything, always a miss
// Designed as a mock for testing
type missingProvider struct{}

// NewMissingProvider returns a provider which does not store anything and always misses.
func NewMissingProvider() lite.Provider {
	return missingProvider{}
}

func (missingProvider) StoreCommit(lite.FullCommit) error { return nil }
func (missingProvider) GetByHeight(int64) (lite.FullCommit, error) {
	return lite.FullCommit{}, liteErr.ErrCommitNotFound()
}
func (missingProvider) GetByHash([]byte) (lite.FullCommit, error) {
	return lite.FullCommit{}, liteErr.ErrCommitNotFound()
}
func (missingProvider) LatestCommit() (lite.FullCommit, error) {
	return lite.FullCommit{}, liteErr.ErrCommitNotFound()
}

func TestMemProvider(t *testing.T) {
	p := lite.NewMemStoreProvider()
	checkProvider(t, p, "test-mem", "empty")
}

func TestCacheProvider(t *testing.T) {
	p := lite.NewCacheProvider(
		NewMissingProvider(),
		lite.NewMemStoreProvider(),
		NewMissingProvider(),
	)
	checkProvider(t, p, "test-cache", "kjfhekfhkewhgit")
}

func checkProvider(t *testing.T, p lite.Provider, chainID, app string) {
	assert, require := assert.New(t), require.New(t)
	appHash := []byte(app)
	keys := lite.GenValKeys(5)
	count := 10

	// make a bunch of commits...
	commits := make([]lite.FullCommit, count)
	for i := 0; i < count; i++ {
		// two commits for each validator, to check how we handle dups
		// (10, 0), (10, 1), (10, 1), (10, 2), (10, 2), ...
		vals := keys.ToValidators(10, int64(count/2))
		h := int64(20 + 10*i)
		commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, []byte("params"), []byte("results"), 0, 5)
	}

	// check provider is empty
	fc, err := p.GetByHeight(20)
	require.NotNil(err)
	assert.True(liteErr.IsCommitNotFoundErr(err))

	fc, err = p.GetByHash(commits[3].ValidatorsHash())
	require.NotNil(err)
	assert.True(liteErr.IsCommitNotFoundErr(err))

	// now add them all to the provider
	for _, s := range commits {
		err = p.StoreCommit(s)
		require.Nil(err)
		// and make sure we can get it back
		s2, err := p.GetByHash(s.ValidatorsHash())
		assert.Nil(err)
		assert.Equal(s, s2)
		// by height as well
		s2, err = p.GetByHeight(s.Height())
		assert.Nil(err)
		assert.Equal(s, s2)
	}

	// make sure we get the last hash if we overstep
	fc, err = p.GetByHeight(5000)
	if assert.Nil(err) {
		assert.Equal(commits[count-1].Height(), fc.Height())
		assert.Equal(commits[count-1], fc)
	}

	// and middle ones as well
	fc, err = p.GetByHeight(47)
	if assert.Nil(err) {
		// we only step by 10, so 40 must be the one below this
		assert.EqualValues(40, fc.Height())
	}

}

// this will make a get height, and if it is good, set the data as well
func checkGetHeight(t *testing.T, p lite.Provider, ask, expect int64) {
	fc, err := p.GetByHeight(ask)
	require.Nil(t, err, "GetByHeight")
	if assert.Equal(t, expect, fc.Height()) {
		err = p.StoreCommit(fc)
		require.Nil(t, err, "StoreCommit")
	}
}

func TestCacheGetsBestHeight(t *testing.T) {
	// assert, require := assert.New(t), require.New(t)
	require := require.New(t)

	// we will write data to the second level of the cache (p2),
	// and see what gets cached, stored in
	p := lite.NewMemStoreProvider()
	p2 := lite.NewMemStoreProvider()
	cp := lite.NewCacheProvider(p, p2)

	chainID := "cache-best-height"
	appHash := []byte("01234567")
	keys := lite.GenValKeys(5)
	count := 10

	// set a bunch of commits
	for i := 0; i < count; i++ {
		vals := keys.ToValidators(10, int64(count/2))
		h := int64(10 * (i + 1))
		fc := keys.GenFullCommit(chainID, h, nil, vals, appHash, []byte("params"), []byte("results"), 0, 5)
		err := p2.StoreCommit(fc)
		require.NoError(err)
	}

	// let's get a few heights from the cache and set them proper
	checkGetHeight(t, cp, 57, 50)
	checkGetHeight(t, cp, 33, 30)

	// make sure they are set in p as well (but nothing else)
	checkGetHeight(t, p, 44, 30)
	checkGetHeight(t, p, 50, 50)
	checkGetHeight(t, p, 99, 50)

	// now, query the cache for a higher value
	checkGetHeight(t, p2, 99, 90)
	checkGetHeight(t, cp, 99, 90)
}
