package keeper_test

import (
	"fmt"
	"testing"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/kava-labs/kava/x/kavamint/keeper"
	"github.com/kava-labs/kava/x/kavamint/types"
	"github.com/stretchr/testify/suite"
)

type InflationTestSuite struct {
	suite.Suite
}

func TestInflationTestSuite(t *testing.T) {
	suite.Run(t, new(InflationTestSuite))
}

func (suite *InflationTestSuite) TestCalculateInflationFactor() {
	testCases := []struct {
		name          string
		apy           sdk.Dec
		secondsPassed uint64
		expectedRate  sdk.Dec
		// preciseToDecimal is the decimal place after which error is present. if >=18, number is exact.
		// ex. precise to 3 decimals means accurate to +/-0.0005
		preciseToDecimal int
	}{
		{
			name:             "any apy over 0 seconds is 0",
			apy:              sdk.OneDec(),
			secondsPassed:    0,
			expectedRate:     sdk.ZeroDec(),
			preciseToDecimal: 19,
		},
		{
			name:             "zero apy for any number of seconds is 0",
			apy:              sdk.ZeroDec(),
			secondsPassed:    100,
			expectedRate:     sdk.ZeroDec(),
			preciseToDecimal: 19,
		},
		{
			name:             "an apy over a year's worth of seconds should be the apy",
			apy:              sdk.NewDecWithPrec(10, 2),
			secondsPassed:    keeper.SecondsPerYear,
			expectedRate:     sdk.NewDecWithPrec(10, 2),
			preciseToDecimal: 10,
		},
		{
			name:             "example: 22 percent for 6 seconds, precise to 17 decimals",
			apy:              sdk.NewDecWithPrec(22, 2),
			secondsPassed:    6,
			expectedRate:     sdk.MustNewDecFromStr("0.000000037833116915"),
			preciseToDecimal: 17,
		},
		{
			name:             "example: 3 percent for 10 seconds, precise to 17 decimals",
			apy:              sdk.NewDecWithPrec(3, 2),
			secondsPassed:    10,
			expectedRate:     sdk.MustNewDecFromStr("0.000000009373034748"),
			preciseToDecimal: 17,
		},
		{
			name:             "example: 150 percent for 10 seconds, precise to 17 decimals",
			apy:              sdk.NewDecWithPrec(150, 2),
			secondsPassed:    10,
			expectedRate:     sdk.MustNewDecFromStr("0.000000290553927255"),
			preciseToDecimal: 17,
		},
		{
			name:             "example: 10 percent for 10 seconds, precise to 16 decimals",
			apy:              sdk.NewDecWithPrec(10, 2),
			secondsPassed:    10,
			expectedRate:     sdk.MustNewDecFromStr("0.000000030222660212"),
			preciseToDecimal: 16,
		},
		{
			name:             "example: 1,000 percent for 10 seconds, precise to 16 decimals",
			apy:              sdk.NewDecWithPrec(1000, 2),
			secondsPassed:    10,
			expectedRate:     sdk.MustNewDecFromStr("0.000000760367892072"),
			preciseToDecimal: 16,
		},
		{
			name:             "example: 10 percent for 30 seconds, precise to 16 decimals",
			apy:              sdk.NewDecWithPrec(10, 2),
			secondsPassed:    30,
			expectedRate:     sdk.MustNewDecFromStr("0.00000009066798338"),
			preciseToDecimal: 16,
		},
		{
			name:             "example: 95 percent for 15 seconds, precise to 16 decimals",
			apy:              sdk.NewDecWithPrec(95, 2),
			secondsPassed:    15,
			expectedRate:     sdk.MustNewDecFromStr("0.000000317651007726"),
			preciseToDecimal: 16,
		},
		{
			name:             "example: 10,000 percent for 10 seconds, precise to 13 decimals",
			apy:              sdk.NewDecWithPrec(10000, 2),
			secondsPassed:    10,
			expectedRate:     sdk.MustNewDecFromStr("0.000001463446186527"),
			preciseToDecimal: 13,
		},
		{
			name:             "can handle upper bound of APY (but w/ large error)",
			apy:              types.MaxMintingRate,
			secondsPassed:    10,
			expectedRate:     sdk.MustNewDecFromStr("0.000001642242155465"),
			preciseToDecimal: 4, // NOTE: error is really large here
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			minter := keeper.NewMinter(tc.name, tc.apy, sdk.ZeroInt(), "ukava", "ignored")
			actualRate, err := minter.CalculateInflationRate(tc.secondsPassed)
			suite.Require().NoError(err)

			marginOfError := sdk.ZeroDec()
			if tc.preciseToDecimal < 18 {
				marginOfError = sdk.NewDecWithPrec(5, int64(tc.preciseToDecimal+1))
			}
			suite.requireWithinError(tc.expectedRate, actualRate, marginOfError)
		})
	}

	suite.Run("errors with out-of-bounds when rate is too high", func() {
		// Dec.ApproxRoot will error w/ out-of-bounds when rate is >176.5
		oob := sdk.NewDec(177)
		minter := keeper.NewMinter(
			"out-of-bounds-minter",
			oob,
			sdk.OneInt(),
			"uakva",
			"ignored",
		)
		_, err := minter.CalculateInflationRate(100)
		suite.Error(err)

		// ensure max mint rate is less than out-of-bounds value
		suite.True(types.MaxMintingRate.LT(oob))
	})
}

func (suite *InflationTestSuite) Test_AccumulateInflation_FailsWithInvalidRate() {
	oob := sdk.NewDec(177)
	minter := keeper.NewMinter(
		"out-of-bounds-minter",
		oob,
		sdk.OneInt(),
		"uakva",
		"ignored",
	)
	_, err := minter.AccumulateInflation(1)
	suite.ErrorContains(err, "out of bounds")
}

func (suite *InflationTestSuite) requireWithinError(expected, actual, margin sdk.Dec) {
	suite.Require().Truef(
		actual.Sub(expected).Abs().LTE(margin),
		fmt.Sprintf("precision is outside desired margin of error %s\nexpected: %s\nactual  : %s", margin, expected, actual),
	)
}