package keeper_test

import (
	"fmt"
	"testing"
	"time"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/stretchr/testify/suite"
	abcitypes "github.com/tendermint/tendermint/abci/types"
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
	tmtime "github.com/tendermint/tendermint/types/time"

	"github.com/kava-labs/kava/app"
	"github.com/kava-labs/kava/x/community/keeper"
	"github.com/kava-labs/kava/x/community/testutil"
	"github.com/kava-labs/kava/x/community/types"
	hardkeeper "github.com/kava-labs/kava/x/hard/keeper"
	hardtypes "github.com/kava-labs/kava/x/hard/types"
	pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)

const chainID = "kavatest_2221-1"

func ukava(amt int64) sdk.Coins {
	return sdk.NewCoins(sdk.NewInt64Coin("ukava", amt))
}
func usdx(amt int64) sdk.Coins {
	return sdk.NewCoins(sdk.NewInt64Coin("usdx", amt))
}
func otherdenom(amt int64) sdk.Coins {
	return sdk.NewCoins(sdk.NewInt64Coin("other-denom", amt))
}

type proposalTestSuite struct {
	suite.Suite

	App         app.TestApp
	Ctx         sdk.Context
	Keeper      keeper.Keeper
	MaccAddress sdk.AccAddress

	hardKeeper hardkeeper.Keeper
}

func TestProposalTestSuite(t *testing.T) {
	suite.Run(t, new(proposalTestSuite))
}

func (suite *proposalTestSuite) SetupTest() {
	app.SetSDKConfig()

	genTime := tmtime.Now()

	hardGS, pricefeedGS := testutil.NewLendGenesisBuilder().
		WithMarket("ukava", "kava:usd", sdk.OneDec()).
		WithMarket("usdx", "usdx:usd", sdk.OneDec()).
		Build()

	tApp := app.NewTestApp()
	ctx := tApp.NewContext(true, tmproto.Header{
		Height:  1,
		Time:    genTime,
		ChainID: chainID,
	})

	tApp.InitializeFromGenesisStatesWithTimeAndChainID(
		genTime, chainID,
		app.GenesisState{hardtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&hardGS)},
		app.GenesisState{pricefeedtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&pricefeedGS)},
	)

	suite.App = tApp
	suite.Ctx = ctx
	suite.Keeper = tApp.GetCommunityKeeper()
	suite.MaccAddress = tApp.GetAccountKeeper().GetModuleAddress(types.ModuleAccountName)
	suite.hardKeeper = suite.App.GetHardKeeper()

	// give the community pool some funds
	// ukava
	suite.FundCommunityPool(ukava(1e10))
	// usdx
	suite.FundCommunityPool(usdx(1e10))
	// other-denom
	suite.FundCommunityPool(otherdenom(1e10))
}

func (suite *proposalTestSuite) NextBlock() {
	newTime := suite.Ctx.BlockTime().Add(6 * time.Second)
	newHeight := suite.Ctx.BlockHeight() + 1

	suite.App.EndBlocker(suite.Ctx, abcitypes.RequestEndBlock{})
	suite.Ctx = suite.Ctx.WithBlockTime(newTime).WithBlockHeight(newHeight).WithChainID(chainID)
	suite.App.BeginBlocker(suite.Ctx, abcitypes.RequestBeginBlock{})
}

func (suite *proposalTestSuite) FundCommunityPool(coins sdk.Coins) {
	// mint to ephemeral account
	ephemeralAcc := app.RandomAddress()
	suite.NoError(suite.App.FundAccount(suite.Ctx, ephemeralAcc, coins))
	// fund community pool with newly minted coins
	suite.NoError(suite.App.GetDistrKeeper().FundCommunityPool(suite.Ctx, coins, ephemeralAcc))
}

func (suite *proposalTestSuite) GetCommunityPoolBalance() sdk.Coins {
	balance, change := suite.App.GetDistrKeeper().GetFeePoolCommunityCoins(suite.Ctx).TruncateDecimal()
	// expect no decimal dust
	suite.True(sdk.NewDecCoins().IsEqual(change), "expected no decimal dust in community pool")
	return balance
}

func (suite *proposalTestSuite) CheckCommunityPoolBalance(expected sdk.Coins) {
	actual := suite.GetCommunityPoolBalance()
	// check that balance is expected
	suite.True(expected.IsEqual(actual), fmt.Sprintf("unexpected balance in community pool\nexpected: %s\nactual: %s", expected, actual))
}

func (suite *proposalTestSuite) TestCommunityLendDepositProposal() {
	testCases := []struct {
		name             string
		proposals        []*types.CommunityPoolLendDepositProposal
		expectedErr      string
		expectedDeposits []sdk.Coins
	}{
		{
			name: "valid - one proposal, one denom",
			proposals: []*types.CommunityPoolLendDepositProposal{
				{Amount: ukava(1e8)},
			},
			expectedErr:      "",
			expectedDeposits: []sdk.Coins{ukava(1e8)},
		},
		{
			name: "valid - one proposal, multiple denoms",
			proposals: []*types.CommunityPoolLendDepositProposal{
				{Amount: ukava(1e8).Add(usdx(1e8)...)},
			},
			expectedErr:      "",
			expectedDeposits: []sdk.Coins{ukava(1e8).Add(usdx(1e8)...)},
		},
		{
			name: "valid - multiple proposals, same denom",
			proposals: []*types.CommunityPoolLendDepositProposal{
				{Amount: ukava(1e8)},
				{Amount: ukava(1e9)},
			},
			expectedErr:      "",
			expectedDeposits: []sdk.Coins{ukava(1e8 + 1e9)},
		},
		{
			name: "valid - multiple proposals, different denoms",
			proposals: []*types.CommunityPoolLendDepositProposal{
				{Amount: ukava(1e8)},
				{Amount: usdx(1e8)},
			},
			expectedErr:      "",
			expectedDeposits: []sdk.Coins{ukava(1e8).Add(usdx(1e8)...)},
		},
		{
			name: "invalid - insufficient balance",
			proposals: []*types.CommunityPoolLendDepositProposal{
				{
					Description: "more coins than i have!",
					Amount:      ukava(1e11),
				},
			},
			expectedErr:      "community pool does not have sufficient coins to distribute",
			expectedDeposits: []sdk.Coins{},
		},
		{
			name: "invalid - invalid lend deposit (unsupported denom)",
			proposals: []*types.CommunityPoolLendDepositProposal{
				{Amount: otherdenom(1e9)},
			},
			expectedErr:      "invalid deposit denom",
			expectedDeposits: []sdk.Coins{},
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			suite.SetupTest()

			for _, proposal := range tc.proposals {
				err := keeper.HandleCommunityPoolLendDepositProposal(suite.Ctx, suite.Keeper, proposal)
				if tc.expectedErr == "" {
					suite.NoError(err)
				} else {
					suite.ErrorContains(err, tc.expectedErr)
				}
			}

			deposits := suite.hardKeeper.GetDepositsByUser(suite.Ctx, suite.MaccAddress)
			suite.Len(deposits, len(tc.expectedDeposits), "expected a deposit to lend")
			for _, amt := range tc.expectedDeposits {
				suite.Equal(amt, deposits[0].Amount, "expected amount to match")
			}
		})
	}
}

func (suite *proposalTestSuite) TestCommunityLendWithdrawProposal() {
	testCases := []struct {
		name               string
		initialDeposit     sdk.Coins
		proposals          []*types.CommunityPoolLendWithdrawProposal
		expectedErr        string
		expectedWithdrawal sdk.Coins
	}{
		{
			// in the week it would take a proposal to pass, the position would have grown
			// to withdraw the entire position, it'd be safest to set a very high withdraw
			name:           "valid - requesting withdrawal of more than total will withdraw all",
			initialDeposit: ukava(1e9),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: ukava(1e12)},
			},
			expectedErr:        "",
			expectedWithdrawal: ukava(1e9),
		},
		{
			name:           "valid - single proposal, single denom, full withdrawal",
			initialDeposit: ukava(1e9),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: ukava(1e9)},
			},
			expectedErr:        "",
			expectedWithdrawal: ukava(1e9),
		},
		{
			name:           "valid - single proposal, multiple denoms, full withdrawal",
			initialDeposit: ukava(1e9).Add(usdx(1e9)...),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: ukava(1e9).Add(usdx(1e9)...)},
			},
			expectedErr:        "",
			expectedWithdrawal: ukava(1e9).Add(usdx(1e9)...),
		},
		{
			name:           "valid - single proposal, partial withdrawal",
			initialDeposit: ukava(1e9).Add(usdx(1e9)...),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: ukava(1e8).Add(usdx(1e9)...)},
			},
			expectedErr:        "",
			expectedWithdrawal: ukava(1e8).Add(usdx(1e9)...),
		},
		{
			name:           "valid - multiple proposals, full withdrawal",
			initialDeposit: ukava(1e9).Add(usdx(1e9)...),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: ukava(1e9)},
				{Amount: usdx(1e9)},
			},
			expectedErr:        "",
			expectedWithdrawal: ukava(1e9).Add(usdx(1e9)...),
		},
		{
			name:           "valid - multiple proposals, partial withdrawal",
			initialDeposit: ukava(1e9).Add(usdx(1e9)...),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: ukava(1e8)},
				{Amount: usdx(1e8)},
			},
			expectedErr:        "",
			expectedWithdrawal: ukava(1e8).Add(usdx(1e8)...),
		},
		{
			name:           "invalid - nonexistent position, has no deposits",
			initialDeposit: sdk.NewCoins(),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: ukava(1e8)},
			},
			expectedErr:        "deposit not found",
			expectedWithdrawal: sdk.NewCoins(),
		},
		{
			name:           "invalid - nonexistent position, has deposits of different denom",
			initialDeposit: ukava(1e8),
			proposals: []*types.CommunityPoolLendWithdrawProposal{
				{Amount: usdx(1e8)},
			},
			expectedErr:        "no coins of this type deposited",
			expectedWithdrawal: sdk.NewCoins(),
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			suite.SetupTest()

			// setup initial deposit
			if !tc.initialDeposit.IsZero() {
				deposit := types.NewCommunityPoolLendDepositProposal("initial deposit", "has coins", tc.initialDeposit)
				err := keeper.HandleCommunityPoolLendDepositProposal(suite.Ctx, suite.Keeper, deposit)
				suite.NoError(err, "unexpected error while seeding lend deposit")
			}

			beforeBalance := suite.GetCommunityPoolBalance()

			// run the proposals
			for i, proposal := range tc.proposals {
				fmt.Println("submitting proposal ", i, " ", suite.Ctx.ChainID())
				err := keeper.HandleCommunityPoolLendWithdrawProposal(suite.Ctx, suite.Keeper, proposal)
				if tc.expectedErr == "" {
					suite.NoError(err)
				} else {
					suite.ErrorContains(err, tc.expectedErr)
				}

				suite.NextBlock()
			}

			// expect funds to be removed from hard deposit
			expectedRemaining := tc.initialDeposit.Sub(tc.expectedWithdrawal)
			deposits := suite.hardKeeper.GetDepositsByUser(suite.Ctx, suite.MaccAddress)
			if expectedRemaining.IsZero() {
				suite.Len(deposits, 0, "expected all deposits to be withdrawn")
			} else {
				suite.Len(deposits, 1, "expected user to have remaining deposit")
				suite.Equal(expectedRemaining, deposits[0].Amount)
			}

			// expect funds to be distributed back to community pool
			suite.CheckCommunityPoolBalance(beforeBalance.Add(tc.expectedWithdrawal...))
		})
	}
}
