package keeper_test

import (
	"time"

	sdk "github.com/cosmos/cosmos-sdk/types"

	abci "github.com/tendermint/tendermint/abci/types"
	tmtime "github.com/tendermint/tendermint/types/time"

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

func (suite *KeeperTestSuite) TestExpireRewardPeriod() {
	rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
	suite.keeper.SetRewardPeriod(suite.ctx, rp)
	suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
	suite.NotPanics(func() {
		suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
	})
	_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
	suite.True(found)
}

func (suite *KeeperTestSuite) TestAddToClaim() {
	rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
	suite.keeper.SetRewardPeriod(suite.ctx, rp)
	suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
	suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
	c1 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
	suite.keeper.SetClaim(suite.ctx, c1)
	suite.NotPanics(func() {
		suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "bnb", 1, c("ukava", 1000000))
	})
	testC, _ := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
	suite.Equal(c("ukava", 2000000), testC.Reward)

	suite.NotPanics(func() {
		suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "xpr", 1, c("ukava", 1000000))
	})
}

func (suite *KeeperTestSuite) TestCreateRewardPeriod() {
	reward := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
	suite.NotPanics(func() {
		suite.keeper.CreateNewRewardPeriod(suite.ctx, reward)
	})
	_, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
	suite.True(found)
}

func (suite *KeeperTestSuite) TestCreateAndDeleteRewardsPeriods() {
	reward1 := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
	reward2 := types.NewReward(false, "xrp", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
	reward3 := types.NewReward(false, "btc", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
	// add a reward period to the store for a non-active reward
	suite.NotPanics(func() {
		suite.keeper.CreateNewRewardPeriod(suite.ctx, reward3)
	})
	params := types.NewParams(true, types.Rewards{reward1, reward2, reward3})
	suite.keeper.SetParams(suite.ctx, params)

	suite.NotPanics(func() {
		suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
	})
	testCases := []struct {
		name        string
		arg         string
		expectFound bool
	}{
		{
			"active reward period",
			"bnb",
			true,
		},
		{
			"attempt to add inactive reward period",
			"xrp",
			false,
		},
		{
			"remove inactive reward period",
			"btc",
			false,
		},
	}
	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			_, found := suite.keeper.GetRewardPeriod(suite.ctx, tc.arg)
			if tc.expectFound {
				suite.True(found)
			} else {
				suite.False(found)
			}
		})
	}
}

func (suite *KeeperTestSuite) TestApplyRewardsToCdps() {
	suite.setupCdpChain() // creates a test app with 3 BNB cdps and usdx incentives for bnb - each reward period is one week

	// move the context forward by 100 periods
	suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
	// apply rewards to BNB cdps
	suite.NotPanics(func() {
		suite.keeper.ApplyRewardsToCdps(suite.ctx)
	})
	// each cdp should have a claim
	claims := types.Claims{}
	suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
		claims = append(claims, c)
		return false
	})
	suite.Equal(3, len(claims))
	// there should be no associated claim period, because the reward period has not ended yet
	_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
	suite.False(found)

	// move ctx to the reward period expiry and check that the claim period has been created and the next claim period id has increased
	suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 24 * 7))

	suite.NotPanics(func() {
		// apply rewards to cdps
		suite.keeper.ApplyRewardsToCdps(suite.ctx)
		// delete the old reward period amd create a new one
		suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
	})
	_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
	suite.True(found)
	testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
	suite.Equal(uint64(2), testID)

	// move the context forward by 100 periods
	suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
	// run the begin blocker functions
	suite.NotPanics(func() {
		suite.keeper.DeleteExpiredClaimsAndClaimPeriods(suite.ctx)
		suite.keeper.ApplyRewardsToCdps(suite.ctx)
		suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
	})
	// each cdp should now have two claims
	claims = types.Claims{}
	suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
		claims = append(claims, c)
		return false
	})
	suite.Equal(6, len(claims))
}

func (suite *KeeperTestSuite) setupCdpChain() {
	// creates a new test app with bnb as the only asset the pricefeed and cdp modules
	// funds three addresses and creates 3 cdps, funded with 100 BNB, 1000 BNB, and 10000 BNB
	// each CDP draws 10, 100, and 1000 USDX respectively
	// adds usdx incentives for bnb - 1000 KAVA per week with a 1 year time lock

	tApp := app.NewTestApp()
	ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
	// need pricefeed and cdp gen state with one collateral
	pricefeedGS := pricefeed.GenesisState{
		Params: pricefeed.Params{
			Markets: []pricefeed.Market{
				{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
			},
		},
		PostedPrices: []pricefeed.PostedPrice{
			{
				MarketID:      "bnb:usd",
				OracleAddress: sdk.AccAddress{},
				Price:         d("12.29"),
				Expiry:        time.Now().Add(100000 * time.Hour),
			},
		},
	}
	// need incentive params for one collateral
	cdpGS := cdp.GenesisState{
		Params: cdp.Params{
			GlobalDebtLimit:              sdk.NewInt64Coin("usdx", 1000000000000),
			SurplusAuctionThreshold:      cdp.DefaultSurplusThreshold,
			DebtAuctionThreshold:         cdp.DefaultDebtThreshold,
			SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
			CollateralParams: cdp.CollateralParams{
				{
					Denom:              "bnb",
					LiquidationRatio:   sdk.MustNewDecFromStr("2.0"),
					DebtLimit:          sdk.NewInt64Coin("usdx", 1000000000000),
					StabilityFee:       sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
					LiquidationPenalty: d("0.05"),
					AuctionSize:        i(10000000000),
					Prefix:             0x20,
					MarketID:           "bnb:usd",
					ConversionFactor:   i(8),
				},
			},
			DebtParam: cdp.DebtParam{
				Denom:            "usdx",
				ReferenceAsset:   "usd",
				ConversionFactor: i(6),
				DebtFloor:        i(10000000),
				SavingsRate:      d("0.95"),
			},
		},
		StartingCdpID:            cdp.DefaultCdpStartingID,
		DebtDenom:                cdp.DefaultDebtDenom,
		GovDenom:                 cdp.DefaultGovDenom,
		CDPs:                     cdp.CDPs{},
		PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
	}
	incentiveGS := types.NewGenesisState(
		types.NewParams(
			true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)},
		),
		types.DefaultPreviousBlockTime,
		types.RewardPeriods{types.NewRewardPeriod("bnb", ctx.BlockTime(), ctx.BlockTime().Add(time.Hour*7*24), c("ukava", 1000), ctx.BlockTime().Add(time.Hour*7*24*2), time.Hour*365*24)},
		types.ClaimPeriods{},
		types.Claims{},
		types.GenesisClaimPeriodIDs{})
	pricefeedAppGs := app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}
	cdpAppGs := app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGS)}
	incentiveAppGs := app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(incentiveGS)}
	_, addrs := app.GeneratePrivKeyAddressPairs(3)
	authGS := app.NewAuthGenState(
		addrs[0:3],
		[]sdk.Coins{
			cs(c("bnb", 10000000000)),
			cs(c("bnb", 100000000000)),
			cs(c("bnb", 1000000000000)),
		})
	tApp.InitializeFromGenesisStates(
		authGS,
		pricefeedAppGs,
		incentiveAppGs,
		cdpAppGs,
	)
	suite.app = tApp
	suite.keeper = tApp.GetIncentiveKeeper()
	suite.ctx = ctx
	// create 3 cdps
	cdpKeeper := tApp.GetCDPKeeper()
	err := cdpKeeper.AddCdp(suite.ctx, addrs[0], c("bnb", 10000000000), c("usdx", 10000000))
	suite.Require().NoError(err)
	err = cdpKeeper.AddCdp(suite.ctx, addrs[1], c("bnb", 100000000000), c("usdx", 100000000))
	suite.Require().NoError(err)
	err = cdpKeeper.AddCdp(suite.ctx, addrs[2], c("bnb", 1000000000000), c("usdx", 1000000000))
	suite.Require().NoError(err)
	// total usd is 1110

	// set the previous block time
	suite.keeper.SetPreviousBlockTime(suite.ctx, suite.ctx.BlockTime())
}

// Avoid cluttering test cases with long function names
func i(in int64) sdk.Int                    { return sdk.NewInt(in) }
func d(str string) sdk.Dec                  { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins        { return sdk.NewCoins(coins...) }