package keeper_test

import (
	"errors"
	"testing"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	hardtypes "github.com/kava-labs/kava/x/hard/types"
	"github.com/kava-labs/kava/x/incentive/keeper"
	"github.com/kava-labs/kava/x/incentive/types"
)

// SynchronizeHardBorrowRewardTests runs unit tests for the keeper.SynchronizeHardBorrowReward method
//
// inputs
// - claim in store (only claim.BorrowRewardIndexes, claim.Reward)
// - global indexes in store
// - borrow function arg (only borrow.Amount)
//
// outputs
// - sets a claim
type SynchronizeHardBorrowRewardTests struct {
	unitTester
}

func TestSynchronizeHardBorrowReward(t *testing.T) {
	suite.Run(t, new(SynchronizeHardBorrowRewardTests))
}

func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenGlobalIndexesHaveIncreased() {
	// This is the normal case

	claim := types.HardLiquidityProviderClaim{
		BaseMultiClaim: types.BaseMultiClaim{
			Owner: arbitraryAddress(),
		},
		BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
	}
	suite.storeHardClaim(claim)

	globalIndexes := increaseAllRewardFactors(nonEmptyMultiRewardIndexes)
	suite.storeGlobalBorrowIndexes(globalIndexes)
	borrow := hardtypes.Borrow{
		Borrower: claim.Owner,
		Amount:   arbitraryCoinsWithDenoms(extractCollateralTypes(claim.BorrowRewardIndexes)...),
	}

	suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)

	syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
	suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
}
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUnchangedWhenGlobalIndexesUnchanged() {
	// It should be safe to call SynchronizeHardBorrowReward multiple times

	unchangingIndexes := nonEmptyMultiRewardIndexes

	claim := types.HardLiquidityProviderClaim{
		BaseMultiClaim: types.BaseMultiClaim{
			Owner: arbitraryAddress(),
		},
		BorrowRewardIndexes: unchangingIndexes,
	}
	suite.storeHardClaim(claim)

	suite.storeGlobalBorrowIndexes(unchangingIndexes)

	borrow := hardtypes.Borrow{
		Borrower: claim.Owner,
		Amount:   arbitraryCoinsWithDenoms(extractCollateralTypes(unchangingIndexes)...),
	}

	suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)

	syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
	suite.Equal(unchangingIndexes, syncedClaim.BorrowRewardIndexes)
}
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardAdded() {
	// When a new reward is added (via gov) for a hard borrow denom the user has already borrowed, and the claim is synced;
	// Then the new reward's index should be added to the claim.

	claim := types.HardLiquidityProviderClaim{
		BaseMultiClaim: types.BaseMultiClaim{
			Owner: arbitraryAddress(),
		},
		BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
	}
	suite.storeHardClaim(claim)

	globalIndexes := appendUniqueMultiRewardIndex(nonEmptyMultiRewardIndexes)
	suite.storeGlobalBorrowIndexes(globalIndexes)

	borrow := hardtypes.Borrow{
		Borrower: claim.Owner,
		Amount:   arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
	}

	suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)

	syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
	suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
}

func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardDenomAdded() {
	// When a new reward coin is added (via gov) to an already rewarded borrow denom (that the user has already borrowed), and the claim is synced;
	// Then the new reward coin's index should be added to the claim.

	claim := types.HardLiquidityProviderClaim{
		BaseMultiClaim: types.BaseMultiClaim{
			Owner: arbitraryAddress(),
		},
		BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
	}
	suite.storeHardClaim(claim)

	globalIndexes := appendUniqueRewardIndexToFirstItem(nonEmptyMultiRewardIndexes)
	suite.storeGlobalBorrowIndexes(globalIndexes)

	borrow := hardtypes.Borrow{
		Borrower: claim.Owner,
		Amount:   arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
	}

	suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)

	syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
	suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
}

func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenGlobalIndexesHaveIncreased() {
	// This is the normal case
	// Given some time has passed (meaning the global indexes have increased)
	// When the claim is synced
	// The user earns rewards for the time passed

	originalReward := arbitraryCoins()

	claim := types.HardLiquidityProviderClaim{
		BaseMultiClaim: types.BaseMultiClaim{
			Owner:  arbitraryAddress(),
			Reward: originalReward,
		},
		BorrowRewardIndexes: types.MultiRewardIndexes{
			{
				CollateralType: "borrowdenom",
				RewardIndexes: types.RewardIndexes{
					{
						CollateralType: "rewarddenom",
						RewardFactor:   d("1000.001"),
					},
				},
			},
		},
	}
	suite.storeHardClaim(claim)

	suite.storeGlobalBorrowIndexes(types.MultiRewardIndexes{
		{
			CollateralType: "borrowdenom",
			RewardIndexes: types.RewardIndexes{
				{
					CollateralType: "rewarddenom",
					RewardFactor:   d("2000.002"),
				},
			},
		},
	})

	borrow := hardtypes.Borrow{
		Borrower: claim.Owner,
		Amount:   cs(c("borrowdenom", 1e9)),
	}

	suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)

	// new reward is (new index - old index) * borrow amount
	syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
	suite.Equal(
		cs(c("rewarddenom", 1_000_001_000_000)).Add(originalReward...),
		syncedClaim.Reward,
	)
}

func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenNewRewardAdded() {
	// When a new reward is added (via gov) for a hard borrow denom the user has already borrowed, and the claim is synced
	// Then the user earns rewards for the time since the reward was added

	originalReward := arbitraryCoins()
	claim := types.HardLiquidityProviderClaim{
		BaseMultiClaim: types.BaseMultiClaim{
			Owner:  arbitraryAddress(),
			Reward: originalReward,
		},
		BorrowRewardIndexes: types.MultiRewardIndexes{
			{
				CollateralType: "rewarded",
				RewardIndexes: types.RewardIndexes{
					{
						CollateralType: "reward",
						RewardFactor:   d("1000.001"),
					},
				},
			},
		},
	}
	suite.storeHardClaim(claim)

	globalIndexes := types.MultiRewardIndexes{
		{
			CollateralType: "rewarded",
			RewardIndexes: types.RewardIndexes{
				{
					CollateralType: "reward",
					RewardFactor:   d("2000.002"),
				},
			},
		},
		{
			CollateralType: "newlyrewarded",
			RewardIndexes: types.RewardIndexes{
				{
					CollateralType: "otherreward",
					// Indexes start at 0 when the reward is added by gov,
					// so this represents the syncing happening some time later.
					RewardFactor: d("1000.001"),
				},
			},
		},
	}
	suite.storeGlobalBorrowIndexes(globalIndexes)

	borrow := hardtypes.Borrow{
		Borrower: claim.Owner,
		Amount:   cs(c("rewarded", 1e9), c("newlyrewarded", 1e9)),
	}

	suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)

	// new reward is (new index - old index) * borrow amount for each borrowed denom
	// The old index for `newlyrewarded` isn't in the claim, so it's added starting at 0 for calculating the reward.
	syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
	suite.Equal(
		cs(c("otherreward", 1_000_001_000_000), c("reward", 1_000_001_000_000)).Add(originalReward...),
		syncedClaim.Reward,
	)
}
func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenNewRewardDenomAdded() {
	// When a new reward coin is added (via gov) to an already rewarded borrow denom (that the user has already borrowed), and the claim is synced;
	// Then the user earns rewards for the time since the reward was added

	originalReward := arbitraryCoins()
	claim := types.HardLiquidityProviderClaim{
		BaseMultiClaim: types.BaseMultiClaim{
			Owner:  arbitraryAddress(),
			Reward: originalReward,
		},
		BorrowRewardIndexes: types.MultiRewardIndexes{
			{
				CollateralType: "borrowed",
				RewardIndexes: types.RewardIndexes{
					{
						CollateralType: "reward",
						RewardFactor:   d("1000.001"),
					},
				},
			},
		},
	}
	suite.storeHardClaim(claim)

	globalIndexes := types.MultiRewardIndexes{
		{
			CollateralType: "borrowed",
			RewardIndexes: types.RewardIndexes{
				{
					CollateralType: "reward",
					RewardFactor:   d("2000.002"),
				},
				{
					CollateralType: "otherreward",
					// Indexes start at 0 when the reward is added by gov,
					// so this represents the syncing happening some time later.
					RewardFactor: d("1000.001"),
				},
			},
		},
	}
	suite.storeGlobalBorrowIndexes(globalIndexes)

	borrow := hardtypes.Borrow{
		Borrower: claim.Owner,
		Amount:   cs(c("borrowed", 1e9)),
	}

	suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)

	// new reward is (new index - old index) * borrow amount for each borrowed denom
	// The old index for `otherreward` isn't in the claim, so it's added starting at 0 for calculating the reward.
	syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
	suite.Equal(
		cs(c("reward", 1_000_001_000_000), c("otherreward", 1_000_001_000_000)).Add(originalReward...),
		syncedClaim.Reward,
	)
}

func TestCalculateRewards(t *testing.T) {
	type expected struct {
		err   error
		coins sdk.Coins
	}
	type args struct {
		oldIndexes, newIndexes types.RewardIndexes
		sourceAmount           sdk.Dec
	}
	testcases := []struct {
		name     string
		args     args
		expected expected
	}{
		{
			name: "when old and new indexes have same denoms, rewards are calculated correctly",
			args: args{
				oldIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.000000001"),
					},
					{
						CollateralType: "ukava",
						RewardFactor:   d("0.1"),
					},
				},
				newIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("1000.0"),
					},
					{
						CollateralType: "ukava",
						RewardFactor:   d("0.100000001"),
					},
				},
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				// for each denom: (new - old) * sourceAmount
				coins: cs(c("hard", 999999999999), c("ukava", 1)),
			},
		},
		{
			name: "when new indexes have an extra denom, rewards are calculated as if it was 0 in old indexes",
			args: args{
				oldIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.000000001"),
					},
				},
				newIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("1000.0"),
					},
					{
						CollateralType: "ukava",
						RewardFactor:   d("0.100000001"),
					},
				},
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				// for each denom: (new - old) * sourceAmount
				coins: cs(c("hard", 999999999999), c("ukava", 100000001)),
			},
		},
		{
			name: "when new indexes are smaller than old, an error is returned",
			args: args{
				oldIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.2"),
					},
				},
				newIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.1"),
					},
				},
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				err: types.ErrDecreasingRewardFactor,
			},
		},
		{
			name: "when old indexes have an extra denom, an error is returned",
			args: args{
				oldIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.1"),
					},
					{
						CollateralType: "ukava",
						RewardFactor:   d("0.1"),
					},
				},
				newIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.2"),
					},
				},
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				err: types.ErrDecreasingRewardFactor,
			},
		},
		{
			name: "when old and new indexes are 0, rewards are 0",
			args: args{
				oldIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.0"),
					},
				},
				newIndexes: types.RewardIndexes{
					{
						CollateralType: "hard",
						RewardFactor:   d("0.0"),
					},
				},
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				coins: nil,
			},
		},
		{
			name: "when old and new indexes are empty, rewards are 0",
			args: args{
				oldIndexes:   types.RewardIndexes{},
				newIndexes:   nil,
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				coins: nil,
			},
		},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			coins, err := keeper.Keeper{}.CalculateRewards(tc.args.oldIndexes, tc.args.newIndexes, tc.args.sourceAmount)
			if tc.expected.err != nil {
				require.True(t, errors.Is(err, tc.expected.err))
			} else {
				require.Equal(t, tc.expected.coins, coins)
			}
		})
	}
}
func TestCalculateSingleReward(t *testing.T) {
	type expected struct {
		err    error
		reward sdk.Int
	}
	type args struct {
		oldIndex, newIndex sdk.Dec
		sourceAmount       sdk.Dec
	}
	testcases := []struct {
		name     string
		args     args
		expected expected
	}{
		{
			name: "when new index is > old, rewards are calculated correctly",
			args: args{
				oldIndex:     d("0.000000001"),
				newIndex:     d("1000.0"),
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				// (new - old) * sourceAmount
				reward: i(999999999999),
			},
		},
		{
			name: "when new index is < old, an error is returned",
			args: args{
				oldIndex:     d("0.000000001"),
				newIndex:     d("0.0"),
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				err: types.ErrDecreasingRewardFactor,
			},
		},
		{
			name: "when old and new indexes are 0, rewards are 0",
			args: args{
				oldIndex:     d("0.0"),
				newIndex:     d("0.0"),
				sourceAmount: d("1000000000"),
			},
			expected: expected{
				reward: sdk.ZeroInt(),
			},
		},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			reward, err := keeper.Keeper{}.CalculateSingleReward(tc.args.oldIndex, tc.args.newIndex, tc.args.sourceAmount)
			if tc.expected.err != nil {
				require.True(t, errors.Is(err, tc.expected.err))
			} else {
				require.Equal(t, tc.expected.reward, reward)
			}
		})
	}
}
