/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package blkstorage

import (
	"testing"
	"time"

	"github.com/hyperledger/fabric-lib-go/common/metrics"
	"github.com/hyperledger/fabric-lib-go/common/metrics/metricsfakes"
	"github.com/hyperledger/fabric/common/ledger/testutil"
	"github.com/stretchr/testify/require"
)

func TestStatsBlockchainHeight(t *testing.T) {
	testMetricProvider := testutilConstructMetricProvider()
	env := newTestEnvWithMetricsProvider(t, NewConf(t.TempDir(), 0), testMetricProvider.fakeProvider)
	defer env.Cleanup()

	provider := env.provider
	ledgerid := "ledger-stats"
	store, err := provider.Open(ledgerid)
	require.NoError(t, err)
	defer store.Shutdown()

	// add genesis block
	blockGenerator, genesisBlock := testutil.NewBlockGenerator(t, "testchannelid", false)
	err = store.AddBlock(genesisBlock)
	require.NoError(t, err)

	// add one more block
	b1 := blockGenerator.NextBlock([][]byte{})
	err = store.AddBlock(b1)
	require.NoError(t, err)

	// should have 3 calls for fakeBlockchainHeightGauge: OpenBlockStore, genesis block, and block b1
	fakeBlockchainHeightGauge := testMetricProvider.fakeBlockchainHeightGauge
	expectedCallCount := 3
	require.Equal(t, expectedCallCount, fakeBlockchainHeightGauge.SetCallCount())

	// verify the call for OpenBlockStore
	require.Equal(t, float64(0), fakeBlockchainHeightGauge.SetArgsForCall(0))
	require.Equal(t, []string{"channel", ledgerid}, fakeBlockchainHeightGauge.WithArgsForCall(0))

	// verify the call for adding genesis block
	require.Equal(t, float64(1), fakeBlockchainHeightGauge.SetArgsForCall(1))
	require.Equal(t, []string{"channel", ledgerid}, fakeBlockchainHeightGauge.WithArgsForCall(1))

	// verify the call for adding block b1
	require.Equal(t, float64(2), fakeBlockchainHeightGauge.SetArgsForCall(2))
	require.Equal(t, []string{"channel", ledgerid}, fakeBlockchainHeightGauge.WithArgsForCall(2))

	// shutdown and reopen the store to verify blockchain height
	store.Shutdown()
	store, err = provider.Open(ledgerid)
	require.NoError(t, err)

	// verify the call when opening an existing ledger - should set height correctly
	require.Equal(t, float64(2), fakeBlockchainHeightGauge.SetArgsForCall(3))
	require.Equal(t, []string{"channel", ledgerid}, fakeBlockchainHeightGauge.WithArgsForCall(3))

	// invoke updateBlockStats api explicitly and verify the call with fake metrics
	store.updateBlockStats(10, 1*time.Second)
	require.Equal(t, float64(11), fakeBlockchainHeightGauge.SetArgsForCall(4))
	require.Equal(t, []string{"channel", ledgerid}, fakeBlockchainHeightGauge.WithArgsForCall(4))
}

func TestStatsBlockCommit(t *testing.T) {
	testMetricProvider := testutilConstructMetricProvider()
	env := newTestEnvWithMetricsProvider(t, NewConf(t.TempDir(), 0), testMetricProvider.fakeProvider)
	defer env.Cleanup()

	provider := env.provider
	ledgerid := "ledger-stats"
	store, err := provider.Open(ledgerid)
	require.NoError(t, err)
	defer store.Shutdown()

	// add a genesis block
	blockGenerator, genesisBlock := testutil.NewBlockGenerator(t, "testchannelid", false)
	err = store.AddBlock(genesisBlock)
	require.NoError(t, err)

	// add 3 more blocks
	for i := 1; i <= 3; i++ {
		b := blockGenerator.NextBlock([][]byte{})
		err = store.AddBlock(b)
		require.NoError(t, err)
	}

	fakeBlockstorageCommitTimeHist := testMetricProvider.fakeBlockstorageCommitTimeHist

	// should have 4 calls to fakeBlockstorageCommitTimeHist: genesis block, and 3 blocks
	expectedCallCount := 1 + 3
	require.Equal(t, expectedCallCount, fakeBlockstorageCommitTimeHist.ObserveCallCount())

	// verify the value of channel in each call (0, 1, 2, 3)
	for i := 0; i < expectedCallCount; i++ {
		require.Equal(t, []string{"channel", ledgerid}, fakeBlockstorageCommitTimeHist.WithArgsForCall(i))
	}

	// invoke updateBlockStats api explicitly and verify with fake metrics (call number is 4)
	store.updateBlockStats(4, 10*time.Second)
	require.Equal(t,
		[]string{"channel", ledgerid},
		testMetricProvider.fakeBlockstorageCommitTimeHist.WithArgsForCall(4),
	)
	require.Equal(t,
		float64(10),
		testMetricProvider.fakeBlockstorageCommitTimeHist.ObserveArgsForCall(4),
	)
}

type testMetricProvider struct {
	fakeProvider                   *metricsfakes.Provider
	fakeBlockchainHeightGauge      *metricsfakes.Gauge
	fakeBlockstorageCommitTimeHist *metricsfakes.Histogram
}

func testutilConstructMetricProvider() *testMetricProvider {
	fakeProvider := &metricsfakes.Provider{}
	fakeBlockchainHeightGauge := testutilConstructGauge()
	fakeBlockstorageCommitTimeHist := testutilConstructHist()
	fakeProvider.NewGaugeStub = func(opts metrics.GaugeOpts) metrics.Gauge {
		switch opts.Name {
		case blockchainHeightOpts.Name:
			return fakeBlockchainHeightGauge
		default:
			return nil
		}
	}
	fakeProvider.NewHistogramStub = func(opts metrics.HistogramOpts) metrics.Histogram {
		switch opts.Name {
		case blockstorageCommitTimeOpts.Name:
			return fakeBlockstorageCommitTimeHist
		default:
			return nil
		}
	}

	return &testMetricProvider{
		fakeProvider,
		fakeBlockchainHeightGauge,
		fakeBlockstorageCommitTimeHist,
	}
}

func testutilConstructGauge() *metricsfakes.Gauge {
	fakeGauge := &metricsfakes.Gauge{}
	fakeGauge.WithStub = func(lableValues ...string) metrics.Gauge {
		return fakeGauge
	}
	return fakeGauge
}

func testutilConstructHist() *metricsfakes.Histogram {
	fakeHist := &metricsfakes.Histogram{}
	fakeHist.WithStub = func(lableValues ...string) metrics.Histogram {
		return fakeHist
	}
	return fakeHist
}
