package testutil

import (
	"testing"
	"time"

	tmprototypes "github.com/cometbft/cometbft/proto/tendermint/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/kava-labs/kava/app"
	"github.com/kava-labs/kava/x/pricefeed/keeper"
	"github.com/kava-labs/kava/x/pricefeed/types"
)

func SetCurrentPrices_PriceCalculations(t *testing.T, f func(ctx sdk.Context, keeper keeper.Keeper)) {
	_, addrs := app.GeneratePrivKeyAddressPairs(5)
	tApp := app.NewTestApp()
	ctx := tApp.NewContext(true, tmprototypes.Header{}).
		WithBlockTime(time.Now().UTC())
	keeper := tApp.GetPriceFeedKeeper()

	params := types.Params{
		Markets: []types.Market{
			// valid previous price, expired prices, price change, and active
			{MarketID: "asset1:usd", BaseAsset: "asset1", QuoteAsset: "usd", Oracles: addrs, Active: true},
			// same data as asset1, but not active and should be ignored
			{MarketID: "asset2:usd", BaseAsset: "asset2", QuoteAsset: "usd", Oracles: addrs, Active: false},
			// same data as asset1 except no valid previous price
			{MarketID: "asset3:usd", BaseAsset: "asset3", QuoteAsset: "usd", Oracles: addrs, Active: true},
			// previous price set, but no valid prices
			{MarketID: "asset4:usd", BaseAsset: "asset4", QuoteAsset: "usd", Oracles: addrs, Active: true},
			// same as market one except different prices
			{MarketID: "asset5:usd", BaseAsset: "asset5", QuoteAsset: "usd", Oracles: addrs, Active: true},
		},
	}
	keeper.SetParams(ctx, params)

	// need price equal to block time and after block time
	blockTime := time.Now()
	initialPriceExpiry := blockTime.Add(1 * time.Hour)

	_, err := keeper.SetPrice(ctx, addrs[0], "asset1:usd", sdk.MustNewDecFromStr("1"), initialPriceExpiry)
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[0], "asset2:usd", sdk.MustNewDecFromStr("1"), initialPriceExpiry)
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[0], "asset4:usd", sdk.MustNewDecFromStr("1"), initialPriceExpiry)
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[0], "asset5:usd", sdk.MustNewDecFromStr("10"), initialPriceExpiry)
	require.NoError(t, err)

	ctx = ctx.WithBlockTime(blockTime)
	f(ctx, keeper)

	// price should be set
	price, err := keeper.GetCurrentPrice(ctx, "asset1:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.OneDec(), price.Price)
	// not an active market, so price is not set
	price, err = keeper.GetCurrentPrice(ctx, "asset2:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// no price posted
	price, err = keeper.GetCurrentPrice(ctx, "asset3:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// price set initially
	price, err = keeper.GetCurrentPrice(ctx, "asset4:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.OneDec(), price.Price)
	price, err = keeper.GetCurrentPrice(ctx, "asset5:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.MustNewDecFromStr("10.0"), price.Price)

	_, err = keeper.SetPrice(ctx, addrs[1], "asset1:usd", sdk.MustNewDecFromStr("2"), initialPriceExpiry.Add(1*time.Hour))
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[1], "asset2:usd", sdk.MustNewDecFromStr("2"), initialPriceExpiry.Add(1*time.Hour))
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[1], "asset5:usd", sdk.MustNewDecFromStr("20"), initialPriceExpiry.Add(1*time.Hour))
	require.NoError(t, err)

	blockTime = blockTime.Add(30 * time.Minute)
	ctx = ctx.WithBlockTime(blockTime)
	f(ctx, keeper)

	// price should be set
	price, err = keeper.GetCurrentPrice(ctx, "asset1:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.MustNewDecFromStr("1.5"), price.Price)
	// not an active market, so price is not set
	price, err = keeper.GetCurrentPrice(ctx, "asset2:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// no price posted
	price, err = keeper.GetCurrentPrice(ctx, "asset3:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// price set initially
	price, err = keeper.GetCurrentPrice(ctx, "asset4:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.OneDec(), price.Price)
	price, err = keeper.GetCurrentPrice(ctx, "asset5:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.MustNewDecFromStr("15.0"), price.Price)

	_, err = keeper.SetPrice(ctx, addrs[2], "asset1:usd", sdk.MustNewDecFromStr("30"), initialPriceExpiry.Add(1*time.Hour))
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[2], "asset2:usd", sdk.MustNewDecFromStr("30"), initialPriceExpiry.Add(1*time.Hour))
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[2], "asset5:usd", sdk.MustNewDecFromStr("30"), initialPriceExpiry.Add(1*time.Hour))
	require.NoError(t, err)

	blockTime = blockTime.Add(15 * time.Minute)
	ctx = ctx.WithBlockTime(blockTime)
	f(ctx, keeper)

	// price should be set
	price, err = keeper.GetCurrentPrice(ctx, "asset1:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.MustNewDecFromStr("2.0"), price.Price)
	// not an active market, so price is not set
	price, err = keeper.GetCurrentPrice(ctx, "asset2:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// no price posted
	price, err = keeper.GetCurrentPrice(ctx, "asset3:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// price set initially
	price, err = keeper.GetCurrentPrice(ctx, "asset4:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.OneDec(), price.Price)
	price, err = keeper.GetCurrentPrice(ctx, "asset5:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.MustNewDecFromStr("20.0"), price.Price)

	blockTime = blockTime.Add(15 * time.Minute)
	ctx = ctx.WithBlockTime(blockTime)
	f(ctx, keeper)

	// price should be set
	price, err = keeper.GetCurrentPrice(ctx, "asset1:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.MustNewDecFromStr("16"), price.Price)
	// not an active market, so price is not set
	price, err = keeper.GetCurrentPrice(ctx, "asset2:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// no price posted
	price, err = keeper.GetCurrentPrice(ctx, "asset3:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	// price set initially, now expired
	price, err = keeper.GetCurrentPrice(ctx, "asset4:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	price, err = keeper.GetCurrentPrice(ctx, "asset5:usd")
	require.NoError(t, err)
	require.Equal(t, sdk.MustNewDecFromStr("25.0"), price.Price)

	blockTime = blockTime.Add(10 * time.Hour)
	ctx = ctx.WithBlockTime(blockTime)
	f(ctx, keeper)

	// all prices expired now
	price, err = keeper.GetCurrentPrice(ctx, "asset1:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	price, err = keeper.GetCurrentPrice(ctx, "asset2:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	price, err = keeper.GetCurrentPrice(ctx, "asset3:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	price, err = keeper.GetCurrentPrice(ctx, "asset4:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
	price, err = keeper.GetCurrentPrice(ctx, "asset5:usd")
	require.Equal(t, types.ErrNoValidPrice, err)
}

func SetCurrentPrices_EventEmission(t *testing.T, f func(ctx sdk.Context, keeper keeper.Keeper)) {
	_, addrs := app.GeneratePrivKeyAddressPairs(5)
	tApp := app.NewTestApp()
	ctx := tApp.NewContext(true, tmprototypes.Header{}).
		WithBlockTime(time.Now().UTC())
	keeper := tApp.GetPriceFeedKeeper()

	params := types.Params{
		Markets: []types.Market{
			{MarketID: "asset1:usd", BaseAsset: "asset1", QuoteAsset: "usd", Oracles: addrs, Active: true},
		},
	}
	keeper.SetParams(ctx, params)

	blockTime := time.Now()
	initialPriceExpiry := blockTime.Add(1 * time.Hour)

	// post a price
	_, err := keeper.SetPrice(ctx, addrs[0], "asset1:usd", sdk.MustNewDecFromStr("1"), initialPriceExpiry)
	require.NoError(t, err)

	// reset context with fresh event manager
	ctx = ctx.WithBlockTime(blockTime).WithEventManager(sdk.NewEventManager())
	f(ctx, keeper)

	// no previous price so no event
	require.Equal(t, 0, len(ctx.EventManager().Events()))

	// post same price from another oracle
	_, err = keeper.SetPrice(ctx, addrs[1], "asset1:usd", sdk.MustNewDecFromStr("1"), initialPriceExpiry)
	require.NoError(t, err)

	blockTime = blockTime.Add(10 * time.Second)
	ctx = ctx.WithBlockTime(blockTime).WithEventManager(sdk.NewEventManager())
	f(ctx, keeper)

	// no price change so no event
	require.Equal(t, 0, len(ctx.EventManager().Events()))

	// post price changes
	_, err = keeper.SetPrice(ctx, addrs[2], "asset1:usd", sdk.MustNewDecFromStr("2"), initialPriceExpiry)
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[3], "asset1:usd", sdk.MustNewDecFromStr("10"), initialPriceExpiry)
	require.NoError(t, err)
	_, err = keeper.SetPrice(ctx, addrs[4], "asset1:usd", sdk.MustNewDecFromStr("10"), initialPriceExpiry)
	require.NoError(t, err)

	blockTime = blockTime.Add(10 * time.Second)
	ctx = ctx.WithBlockTime(blockTime).WithEventManager(sdk.NewEventManager())
	f(ctx, keeper)

	// price is changes so event should be emitted
	require.Equal(t, 1, len(ctx.EventManager().Events()))

	event := ctx.EventManager().Events()[0]

	// has correct event type
	assert.Equal(t, types.EventTypeMarketPriceUpdated, event.Type)
	// has correct attributes
	marketID, found := event.GetAttribute(types.AttributeMarketID)
	require.True(t, found)
	marketPrice, found := event.GetAttribute(types.AttributeMarketPrice)
	require.True(t, found)
	// attributes have correct values
	assert.Equal(t, "asset1:usd", marketID.Value)
	assert.Equal(t, sdk.MustNewDecFromStr("2").String(), marketPrice.Value)
}