package keeper_test

import (
	"time"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/x/auth"
	"github.com/cosmos/cosmos-sdk/x/auth/vesting"
	abci "github.com/tendermint/tendermint/abci/types"
	"github.com/tendermint/tendermint/crypto"

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

func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
	type accountArgs struct {
		addr                 sdk.AccAddress
		vestingAccountBefore bool
		vestingAccountAfter  bool
		coins                sdk.Coins
		periods              vesting.Periods
		origVestingCoins     sdk.Coins
		startTime            int64
		endTime              int64
	}
	type args struct {
		accArgs                   accountArgs
		period                    vesting.Period
		blockTime                 time.Time
		expectedAccountBalance    sdk.Coins
		expectedModAccountBalance sdk.Coins
		expectedPeriods           vesting.Periods
		expectedStartTime         int64
		expectedEndTime           int64
	}
	type errArgs struct {
		expectPass bool
		contains   string
	}
	type testCase struct {
		name    string
		args    args
		errArgs errArgs
	}
	testCases := []testCase{
		{
			name: "send liquid coins to base account",
			args: args{
				accArgs: accountArgs{
					addr:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
					vestingAccountBefore: false,
					vestingAccountAfter:  false,
					coins:                sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
				},
				period:                    vesting.Period{Length: 0, Amount: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)))},
				blockTime:                 time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
				expectedAccountBalance:    sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)), sdk.NewCoin("hard", sdk.NewInt(100))),
				expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
				expectedPeriods:           vesting.Periods{},
				expectedStartTime:         0,
				expectedEndTime:           0,
			},
			errArgs: errArgs{
				expectPass: true,
				contains:   "",
			},
		},
		{
			name: "send liquid coins to vesting account",
			args: args{
				accArgs: accountArgs{
					addr:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
					vestingAccountBefore: true,
					vestingAccountAfter:  true,
					coins:                sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
					periods: vesting.Periods{
						vesting.Period{Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), Length: 100},
					},
					origVestingCoins: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
					startTime:        time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
					endTime:          time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix() + 100,
				},
				period:                    vesting.Period{Length: 0, Amount: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)))},
				blockTime:                 time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
				expectedAccountBalance:    sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)), sdk.NewCoin("hard", sdk.NewInt(100))),
				expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
				expectedPeriods: vesting.Periods{
					vesting.Period{Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), Length: 100},
				},
				expectedStartTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
				expectedEndTime:   time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix() + 100,
			},
			errArgs: errArgs{
				expectPass: true,
				contains:   "",
			},
		},
		{
			name: "insert period at beginning of schedule",
			args: args{
				accArgs: accountArgs{
					addr:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
					vestingAccountBefore: true,
					vestingAccountAfter:  true,
					coins:                cs(c("bnb", 20)),
					periods: vesting.Periods{
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
					origVestingCoins: cs(c("bnb", 20)),
					startTime:        100,
					endTime:          120,
				},
				period:                    vesting.Period{Length: 2, Amount: cs(c("hard", 6))},
				blockTime:                 time.Unix(101, 0),
				expectedAccountBalance:    cs(c("bnb", 20), c("hard", 6)),
				expectedModAccountBalance: cs(c("hard", 994)),
				expectedPeriods: vesting.Periods{
					vesting.Period{Length: 3, Amount: cs(c("hard", 6))},
					vesting.Period{Length: 2, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
				expectedStartTime: 100,
				expectedEndTime:   120,
			},
			errArgs: errArgs{
				expectPass: true,
				contains:   "",
			},
		},
		{
			name: "insert period at beginning with new start time",
			args: args{
				accArgs: accountArgs{
					addr:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
					vestingAccountBefore: true,
					vestingAccountAfter:  true,
					coins:                cs(c("bnb", 20)),
					periods: vesting.Periods{
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
					origVestingCoins: cs(c("bnb", 20)),
					startTime:        100,
					endTime:          120,
				},
				period:                    vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
				blockTime:                 time.Unix(80, 0),
				expectedAccountBalance:    cs(c("bnb", 20), c("hard", 6)),
				expectedModAccountBalance: cs(c("hard", 994)),
				expectedPeriods: vesting.Periods{
					vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
					vesting.Period{Length: 18, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
				expectedStartTime: 80,
				expectedEndTime:   120,
			},
			errArgs: errArgs{
				expectPass: true,
				contains:   "",
			},
		},
		{
			name: "insert period in middle of schedule",
			args: args{
				accArgs: accountArgs{
					addr:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
					vestingAccountBefore: true,
					vestingAccountAfter:  true,
					coins:                cs(c("bnb", 20)),
					periods: vesting.Periods{
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
					origVestingCoins: cs(c("bnb", 20)),
					startTime:        100,
					endTime:          120,
				},
				period:                    vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
				blockTime:                 time.Unix(101, 0),
				expectedAccountBalance:    cs(c("bnb", 20), c("hard", 6)),
				expectedModAccountBalance: cs(c("hard", 994)),
				expectedPeriods: vesting.Periods{
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 3, Amount: cs(c("hard", 6))},
					vesting.Period{Length: 2, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
				expectedStartTime: 100,
				expectedEndTime:   120,
			},
			errArgs: errArgs{
				expectPass: true,
				contains:   "",
			},
		},
		{
			name: "append to end of schedule",
			args: args{
				accArgs: accountArgs{
					addr:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
					vestingAccountBefore: true,
					vestingAccountAfter:  true,
					coins:                cs(c("bnb", 20)),
					periods: vesting.Periods{
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
					origVestingCoins: cs(c("bnb", 20)),
					startTime:        100,
					endTime:          120,
				},
				period:                    vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
				blockTime:                 time.Unix(125, 0),
				expectedAccountBalance:    cs(c("bnb", 20), c("hard", 6)),
				expectedModAccountBalance: cs(c("hard", 994)),
				expectedPeriods: vesting.Periods{
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 12, Amount: cs(c("hard", 6))}},
				expectedStartTime: 100,
				expectedEndTime:   132,
			},
			errArgs: errArgs{
				expectPass: true,
				contains:   "",
			},
		},
		{
			name: "add coins to existing period",
			args: args{
				accArgs: accountArgs{
					addr:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
					vestingAccountBefore: true,
					vestingAccountAfter:  true,
					coins:                cs(c("bnb", 20)),
					periods: vesting.Periods{
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
						vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
					origVestingCoins: cs(c("bnb", 20)),
					startTime:        100,
					endTime:          120,
				},
				period:                    vesting.Period{Length: 5, Amount: cs(c("hard", 6))},
				blockTime:                 time.Unix(110, 0),
				expectedAccountBalance:    cs(c("bnb", 20), c("hard", 6)),
				expectedModAccountBalance: cs(c("hard", 994)),
				expectedPeriods: vesting.Periods{
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5), c("hard", 6))},
					vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
				expectedStartTime: 100,
				expectedEndTime:   120,
			},
			errArgs: errArgs{
				expectPass: true,
				contains:   "",
			},
		},
	}
	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			// create new app with one funded account

			// Initialize test app and set context
			tApp := app.NewTestApp()
			ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
			authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.accArgs.addr}, []sdk.Coins{tc.args.accArgs.coins})
			loanToValue := sdk.MustNewDecFromStr("0.6")
			hardGS := types.NewGenesisState(types.NewParams(
				types.MoneyMarkets{
					types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
					types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
				},
				sdk.NewDec(10),
			), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows,
				types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves,
			)
			tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
			if tc.args.accArgs.vestingAccountBefore {
				ak := tApp.GetAccountKeeper()
				acc := ak.GetAccount(ctx, tc.args.accArgs.addr)
				bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
				bva, err := vesting.NewBaseVestingAccount(bacc, tc.args.accArgs.origVestingCoins, tc.args.accArgs.endTime)
				suite.Require().NoError(err)
				pva := vesting.NewPeriodicVestingAccountRaw(bva, tc.args.accArgs.startTime, tc.args.accArgs.periods)
				ak.SetAccount(ctx, pva)
			}
			supplyKeeper := tApp.GetSupplyKeeper()
			supplyKeeper.MintCoins(ctx, types.ModuleAccountName, sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1000))))
			keeper := tApp.GetHardKeeper()
			suite.app = tApp
			suite.ctx = ctx
			suite.keeper = keeper

			err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, types.ModuleAccountName, tc.args.accArgs.addr, tc.args.period.Amount, tc.args.period.Length)

			if tc.errArgs.expectPass {
				suite.Require().NoError(err)
				acc := suite.getAccount(tc.args.accArgs.addr)
				suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
				mAcc := suite.getModuleAccount(types.ModuleAccountName)
				suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
				vacc, ok := acc.(*vesting.PeriodicVestingAccount)
				if tc.args.accArgs.vestingAccountAfter {
					suite.Require().True(ok)
					suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods)
					suite.Require().Equal(tc.args.expectedStartTime, vacc.StartTime)
					suite.Require().Equal(tc.args.expectedEndTime, vacc.EndTime)
				} else {
					suite.Require().False(ok)
				}
			}

		})
	}
}

func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins        { return sdk.NewCoins(coins...) }