package cdp_test

import (
	"fmt"
	"sort"
	"strings"
	"testing"
	"time"

	abci "github.com/cometbft/cometbft/abci/types"
	tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
	tmtime "github.com/cometbft/cometbft/types/time"
	"github.com/stretchr/testify/suite"

	sdkmath "cosmossdk.io/math"
	sdk "github.com/cosmos/cosmos-sdk/types"

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

type GenesisTestSuite struct {
	suite.Suite

	app     app.TestApp
	ctx     sdk.Context
	genTime time.Time
	keeper  keeper.Keeper
	addrs   []sdk.AccAddress
}

func (suite *GenesisTestSuite) SetupTest() {
	tApp := app.NewTestApp()
	suite.genTime = tmtime.Canonical(time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC))
	suite.ctx = tApp.NewContext(true, tmproto.Header{Height: 1, Time: suite.genTime})
	suite.keeper = tApp.GetCDPKeeper()
	suite.app = tApp

	_, addrs := app.GeneratePrivKeyAddressPairs(3)
	suite.addrs = addrs
}

func (suite *GenesisTestSuite) TestInvalidGenState() {
	type args struct {
		params             types.Params
		cdps               types.CDPs
		deposits           types.Deposits
		startingID         uint64
		debtDenom          string
		govDenom           string
		genAccumTimes      types.GenesisAccumulationTimes
		genTotalPrincipals types.GenesisTotalPrincipals
	}
	type errArgs struct {
		expectPass bool
		contains   string
	}

	testCases := []struct {
		name    string
		args    args
		errArgs errArgs
	}{
		{
			name: "empty debt denom",
			args: args{
				params:             types.DefaultParams(),
				cdps:               types.CDPs{},
				deposits:           types.Deposits{},
				debtDenom:          "",
				govDenom:           types.DefaultGovDenom,
				genAccumTimes:      types.DefaultGenesisState().PreviousAccumulationTimes,
				genTotalPrincipals: types.DefaultGenesisState().TotalPrincipals,
			},
			errArgs: errArgs{
				expectPass: false,
				contains:   "debt denom invalid",
			},
		},
		{
			name: "empty gov denom",
			args: args{
				params:             types.DefaultParams(),
				cdps:               types.CDPs{},
				deposits:           types.Deposits{},
				debtDenom:          types.DefaultDebtDenom,
				govDenom:           "",
				genAccumTimes:      types.DefaultGenesisState().PreviousAccumulationTimes,
				genTotalPrincipals: types.DefaultGenesisState().TotalPrincipals,
			},
			errArgs: errArgs{
				expectPass: false,
				contains:   "gov denom invalid",
			},
		},
		{
			name: "interest factor below one",
			args: args{
				params:             types.DefaultParams(),
				cdps:               types.CDPs{},
				deposits:           types.Deposits{},
				debtDenom:          types.DefaultDebtDenom,
				govDenom:           types.DefaultGovDenom,
				genAccumTimes:      types.GenesisAccumulationTimes{types.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec().Sub(sdk.SmallestDec()))},
				genTotalPrincipals: types.DefaultGenesisState().TotalPrincipals,
			},
			errArgs: errArgs{
				expectPass: false,
				contains:   "interest factor should be ≥ 1.0",
			},
		},
		{
			name: "negative total principal",
			args: args{
				params:             types.DefaultParams(),
				cdps:               types.CDPs{},
				deposits:           types.Deposits{},
				debtDenom:          types.DefaultDebtDenom,
				govDenom:           types.DefaultGovDenom,
				genAccumTimes:      types.DefaultGenesisState().PreviousAccumulationTimes,
				genTotalPrincipals: types.GenesisTotalPrincipals{types.NewGenesisTotalPrincipal("bnb-a", sdkmath.NewInt(-1))},
			},
			errArgs: errArgs{
				expectPass: false,
				contains:   "total principal should be positive",
			},
		},
	}
	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			gs := types.NewGenesisState(tc.args.params, tc.args.cdps, tc.args.deposits, tc.args.startingID,
				tc.args.debtDenom, tc.args.govDenom, tc.args.genAccumTimes, tc.args.genTotalPrincipals)
			err := gs.Validate()
			if tc.errArgs.expectPass {
				suite.Require().NoError(err)
			} else {
				suite.Require().Error(err)
				suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
			}
		})
	}
}

func (suite *GenesisTestSuite) TestValidGenState() {
	cdc := suite.app.AppCodec()

	suite.NotPanics(func() {
		suite.app.InitializeFromGenesisStates(
			NewPricefeedGenStateMulti(cdc),
			NewCDPGenStateMulti(cdc),
		)
	})

	cdpGS := NewCDPGenStateMulti(cdc)
	gs := types.GenesisState{}
	suite.app.AppCodec().MustUnmarshalJSON(cdpGS["cdp"], &gs)
	gs.CDPs = cdps()
	gs.StartingCdpID = uint64(5)
	appGS := app.GenesisState{"cdp": suite.app.AppCodec().MustMarshalJSON(&gs)}
	suite.NotPanics(func() {
		suite.SetupTest()
		suite.app.InitializeFromGenesisStates(
			NewPricefeedGenStateMulti(cdc),
			appGS,
		)
	})
}

func (suite *GenesisTestSuite) Test_InitExportGenesis() {
	cdps := types.CDPs{
		{
			ID:              2,
			Owner:           suite.addrs[0],
			Type:            "xrp-a",
			Collateral:      c("xrp", 200000000),
			Principal:       c("usdx", 10000000),
			AccumulatedFees: c("usdx", 0),
			FeesUpdated:     suite.genTime,
			InterestFactor:  sdk.NewDec(1),
		},
	}

	genTotalPrincipals := types.GenesisTotalPrincipals{
		types.NewGenesisTotalPrincipal("btc-a", sdk.ZeroInt()),
		types.NewGenesisTotalPrincipal("xrp-a", sdk.ZeroInt()),
	}

	var deposits types.Deposits
	for _, c := range cdps {
		deposit := types.Deposit{
			CdpID:     c.ID,
			Depositor: c.Owner,
			Amount:    c.Collateral,
		}
		deposits = append(deposits, deposit)

		for i, p := range genTotalPrincipals {
			if p.CollateralType == c.Type {
				genTotalPrincipals[i].TotalPrincipal = genTotalPrincipals[i].TotalPrincipal.Add(c.Principal.Amount)
			}
		}
	}

	cdpGenesis := types.GenesisState{
		Params: types.Params{
			GlobalDebtLimit:          sdk.NewInt64Coin("usdx", 1000000000000),
			SurplusAuctionThreshold:  types.DefaultSurplusThreshold,
			SurplusAuctionLot:        types.DefaultSurplusLot,
			DebtAuctionThreshold:     types.DefaultDebtThreshold,
			DebtAuctionLot:           types.DefaultDebtLot,
			LiquidationBlockInterval: types.DefaultBeginBlockerExecutionBlockInterval,
			CollateralParams: types.CollateralParams{
				{
					Denom:                            "xrp",
					Type:                             "xrp-a",
					LiquidationRatio:                 sdk.MustNewDecFromStr("2.0"),
					DebtLimit:                        sdk.NewInt64Coin("usdx", 500000000000),
					StabilityFee:                     sdk.MustNewDecFromStr("1.000000001547125958"), // 5% apr
					LiquidationPenalty:               d("0.05"),
					AuctionSize:                      i(7000000000),
					SpotMarketID:                     "xrp:usd",
					LiquidationMarketID:              "xrp:usd",
					KeeperRewardPercentage:           d("0.01"),
					CheckCollateralizationIndexCount: i(10),
					ConversionFactor:                 i(6),
				},
				{
					Denom:                            "btc",
					Type:                             "btc-a",
					LiquidationRatio:                 sdk.MustNewDecFromStr("1.5"),
					DebtLimit:                        sdk.NewInt64Coin("usdx", 500000000000),
					StabilityFee:                     sdk.MustNewDecFromStr("1.000000000782997609"), // 2.5% apr
					LiquidationPenalty:               d("0.025"),
					AuctionSize:                      i(10000000),
					SpotMarketID:                     "btc:usd",
					LiquidationMarketID:              "btc:usd",
					KeeperRewardPercentage:           d("0.01"),
					CheckCollateralizationIndexCount: i(10),
					ConversionFactor:                 i(8),
				},
			},
			DebtParam: types.DebtParam{
				Denom:            "usdx",
				ReferenceAsset:   "usd",
				ConversionFactor: i(6),
				DebtFloor:        i(10000000),
			},
		},
		StartingCdpID: types.DefaultCdpStartingID,
		DebtDenom:     types.DefaultDebtDenom,
		GovDenom:      types.DefaultGovDenom,
		CDPs:          cdps,
		Deposits:      deposits,
		PreviousAccumulationTimes: types.GenesisAccumulationTimes{
			types.NewGenesisAccumulationTime("btc-a", suite.genTime, sdk.OneDec()),
			types.NewGenesisAccumulationTime("xrp-a", suite.genTime, sdk.OneDec()),
		},
		TotalPrincipals: genTotalPrincipals,
	}

	suite.NotPanics(func() {
		suite.app.InitializeFromGenesisStatesWithTime(
			suite.genTime,
			NewPricefeedGenStateMulti(suite.app.AppCodec()),
			app.GenesisState{types.ModuleName: suite.app.AppCodec().MustMarshalJSON(&cdpGenesis)},
		)
	})

	// We run the BeginBlocker at time.Now() to accumulate interest
	suite.ctx = suite.ctx.WithBlockTime(time.Now())
	cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.keeper)

	expectedGenesis := cdpGenesis

	// Update previous accrual times in expected genesis
	var expectedPrevAccTimes types.GenesisAccumulationTimes
	for _, prevAccTime := range cdpGenesis.PreviousAccumulationTimes {
		time, found := suite.keeper.GetPreviousAccrualTime(suite.ctx, prevAccTime.CollateralType)
		if !found {
			panic(fmt.Sprintf("couldn't find previous accrual time for %s", prevAccTime.CollateralType))
		}
		prevAccTime.PreviousAccumulationTime = time

		interestFactor, found := suite.keeper.GetInterestFactor(suite.ctx, prevAccTime.CollateralType)
		if !found {
			panic(fmt.Sprintf("couldn't find interest factor for %s", prevAccTime.CollateralType))
		}
		prevAccTime.InterestFactor = interestFactor

		expectedPrevAccTimes = append(expectedPrevAccTimes, prevAccTime)
	}
	expectedGenesis.PreviousAccumulationTimes = expectedPrevAccTimes

	// Update total principals
	var totalPrincipals types.GenesisTotalPrincipals
	for _, p := range expectedGenesis.TotalPrincipals {
		totalPrincipal := suite.keeper.GetTotalPrincipal(suite.ctx, p.CollateralType, "usdx")
		p.TotalPrincipal = totalPrincipal
		totalPrincipals = append(totalPrincipals, p)
	}
	expectedGenesis.TotalPrincipals = totalPrincipals

	// Update CDPs
	expectedGenesis.CDPs = suite.keeper.GetAllCdps(suite.ctx)

	exportedGenesis := cdp.ExportGenesis(suite.ctx, suite.keeper)

	// Sort TotalPrincipals in both genesis files so slice order matches
	sort.SliceStable(expectedGenesis.TotalPrincipals, func(i, j int) bool {
		return expectedGenesis.TotalPrincipals[i].CollateralType < expectedGenesis.TotalPrincipals[j].CollateralType
	})
	sort.SliceStable(exportedGenesis.TotalPrincipals, func(i, j int) bool {
		return exportedGenesis.TotalPrincipals[i].CollateralType < exportedGenesis.TotalPrincipals[j].CollateralType
	})

	// Sort PreviousAccumulationTimes in both genesis files so slice order matches
	sort.SliceStable(expectedGenesis.PreviousAccumulationTimes, func(i, j int) bool {
		return expectedGenesis.PreviousAccumulationTimes[i].CollateralType < expectedGenesis.PreviousAccumulationTimes[j].CollateralType
	})
	sort.SliceStable(exportedGenesis.PreviousAccumulationTimes, func(i, j int) bool {
		return exportedGenesis.PreviousAccumulationTimes[i].CollateralType < exportedGenesis.PreviousAccumulationTimes[j].CollateralType
	})

	suite.Equal(expectedGenesis, exportedGenesis)
}

func TestGenesisTestSuite(t *testing.T) {
	suite.Run(t, new(GenesisTestSuite))
}