package keeper_test

import (
	"time"

	"cosmossdk.io/math"

	addresscodec "git.cw.tr/mukan-network/mukan-sdk/codec/address"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	"git.cw.tr/mukan-network/mukan-sdk/x/staking/testutil"
	"git.cw.tr/mukan-network/mukan-sdk/x/staking/types"
)

func (s *KeeperTestSuite) TestIncrementUnbondingID() {
	for i := 1; i < 10; i++ {
		id, err := s.stakingKeeper.IncrementUnbondingID(s.ctx)
		s.Require().NoError(err)
		s.Require().Equal(uint64(i), id)
	}
}

func (s *KeeperTestSuite) TestUnbondingTypeAccessors() {
	require := s.Require()
	cases := []struct {
		exists   bool
		name     string
		expected types.UnbondingType
	}{
		{
			name:     "existing 1",
			exists:   true,
			expected: types.UnbondingType_UnbondingDelegation,
		},
		{
			name:     "existing 2",
			exists:   true,
			expected: types.UnbondingType_Redelegation,
		},
		{
			name:   "not existing",
			exists: false,
		},
	}

	for i, tc := range cases {
		s.Run(tc.name, func() {
			if tc.exists {
				require.NoError(s.stakingKeeper.SetUnbondingType(s.ctx, uint64(i), tc.expected))
			}

			unbondingType, err := s.stakingKeeper.GetUnbondingType(s.ctx, uint64(i))
			if tc.exists {
				require.NoError(err)
				require.Equal(tc.expected, unbondingType)
			} else {
				require.ErrorIs(err, types.ErrNoUnbondingType)
			}
		})
	}
}

func (s *KeeperTestSuite) TestUnbondingDelegationByUnbondingIDAccessors() {
	delAddrs, valAddrs := createValAddrs(2)
	require := s.Require()

	type exists struct {
		setUnbondingDelegation              bool
		setUnbondingDelegationByUnbondingID bool
	}

	cases := []struct {
		exists   exists
		name     string
		expected types.UnbondingDelegation
	}{
		{
			name:   "existing 1",
			exists: exists{true, true},
			expected: types.NewUnbondingDelegation(
				delAddrs[0],
				valAddrs[0],
				0,
				time.Unix(0, 0).UTC(),
				math.NewInt(5),
				0,
				addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
			),
		},
		{
			name:   "not existing 1",
			exists: exists{false, true},
			expected: types.NewUnbondingDelegation(
				delAddrs[1],
				valAddrs[1],
				0,
				time.Unix(0, 0).UTC(),
				math.NewInt(5),
				0,
				addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
			),
		},
		{
			name:   "not existing 2",
			exists: exists{false, false},
			expected: types.NewUnbondingDelegation(
				delAddrs[0],
				valAddrs[0],
				0,
				time.Unix(0, 0).UTC(),
				math.NewInt(5),
				0,
				addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
			),
		},
	}

	for i, tc := range cases {
		s.Run(tc.name, func() {
			if tc.exists.setUnbondingDelegation {
				require.NoError(s.stakingKeeper.SetUnbondingDelegation(s.ctx, tc.expected))
			}

			if tc.exists.setUnbondingDelegationByUnbondingID {
				require.NoError(s.stakingKeeper.SetUnbondingDelegationByUnbondingID(s.ctx, tc.expected, uint64(i)))
			}

			ubd, err := s.stakingKeeper.GetUnbondingDelegationByUnbondingID(s.ctx, uint64(i))
			if tc.exists.setUnbondingDelegation && tc.exists.setUnbondingDelegationByUnbondingID {
				require.NoError(err)
				require.Equal(tc.expected, ubd)
			} else {
				require.ErrorIs(err, types.ErrNoUnbondingDelegation)
			}
		})
	}
}

func (s *KeeperTestSuite) TestRedelegationByUnbondingIDAccessors() {
	delAddrs, valAddrs := createValAddrs(2)
	require := s.Require()

	type exists struct {
		setRedelegation              bool
		setRedelegationByUnbondingID bool
	}

	cases := []struct {
		exists   exists
		name     string
		expected types.Redelegation
	}{
		{
			name:   "existing 1",
			exists: exists{true, true},
			expected: types.NewRedelegation(
				delAddrs[0],
				valAddrs[0],
				valAddrs[1],
				0,
				time.Unix(5, 0).UTC(),
				math.NewInt(10),
				math.LegacyNewDec(10),
				0,
				addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
			),
		},
		{
			name:   "not existing 1",
			exists: exists{false, true},
			expected: types.NewRedelegation(
				delAddrs[1],
				valAddrs[0],
				valAddrs[1],
				0,
				time.Unix(5, 0).UTC(),
				math.NewInt(10),
				math.LegacyNewDec(10),
				0,
				addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
			),
		},
		{
			name:   "not existing 2",
			exists: exists{false, false},
			expected: types.NewRedelegation(
				delAddrs[1],
				valAddrs[1],
				valAddrs[0],
				0,
				time.Unix(5, 0).UTC(),
				math.NewInt(10),
				math.LegacyNewDec(10),
				0,
				addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
			),
		},
	}

	for i, tc := range cases {
		s.Run(tc.name, func() {
			if tc.exists.setRedelegation {
				require.NoError(s.stakingKeeper.SetRedelegation(s.ctx, tc.expected))
			}

			if tc.exists.setRedelegationByUnbondingID {
				require.NoError(s.stakingKeeper.SetRedelegationByUnbondingID(s.ctx, tc.expected, uint64(i)))
			}

			red, err := s.stakingKeeper.GetRedelegationByUnbondingID(s.ctx, uint64(i))
			if tc.exists.setRedelegation && tc.exists.setRedelegationByUnbondingID {
				require.NoError(err)
				require.Equal(tc.expected, red)
			} else {
				require.ErrorIs(err, types.ErrNoRedelegation)
			}
		})
	}
}

func (s *KeeperTestSuite) TestValidatorByUnbondingIDAccessors() {
	_, valAddrs := createValAddrs(3)
	require := s.Require()

	type exists struct {
		setValidator              bool
		setValidatorByUnbondingID bool
	}

	cases := []struct {
		exists    exists
		name      string
		validator types.Validator
	}{
		{
			name:      "existing 1",
			exists:    exists{true, true},
			validator: testutil.NewValidator(s.T(), valAddrs[0], PKs[0]),
		},
		{
			name:      "not existing 1",
			exists:    exists{false, true},
			validator: testutil.NewValidator(s.T(), valAddrs[1], PKs[1]),
		},
		{
			name:      "not existing 2",
			exists:    exists{false, false},
			validator: testutil.NewValidator(s.T(), valAddrs[2], PKs[0]),
		},
	}

	for i, tc := range cases {
		s.Run(tc.name, func() {
			if tc.exists.setValidator {
				require.NoError(s.stakingKeeper.SetValidator(s.ctx, tc.validator))
			}

			if tc.exists.setValidatorByUnbondingID {
				require.NoError(s.stakingKeeper.SetValidatorByUnbondingID(s.ctx, tc.validator, uint64(i)))
			}

			val, err := s.stakingKeeper.GetValidatorByUnbondingID(s.ctx, uint64(i))
			if tc.exists.setValidator && tc.exists.setValidatorByUnbondingID {
				require.NoError(err)
				require.Equal(tc.validator, val)
			} else {
				require.ErrorIs(err, types.ErrNoValidatorFound)
			}
		})
	}
}

func (s *KeeperTestSuite) TestUnbondingCanComplete() {
	delAddrs, valAddrs := createValAddrs(3)
	require := s.Require()

	unbondingID := uint64(1)

	// no unbondingID set
	err := s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.ErrorIs(err, types.ErrNoUnbondingType)

	// unbonding delegation
	require.NoError(s.stakingKeeper.SetUnbondingType(s.ctx, unbondingID, types.UnbondingType_UnbondingDelegation))
	err = s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.ErrorIs(err, types.ErrNoUnbondingDelegation)

	ubd := types.NewUnbondingDelegation(
		delAddrs[0],
		valAddrs[0],
		0,
		time.Unix(0, 0).UTC(),
		math.NewInt(5),
		unbondingID,
		addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
	)
	require.NoError(s.stakingKeeper.SetUnbondingDelegation(s.ctx, ubd))
	require.NoError(s.stakingKeeper.SetUnbondingDelegationByUnbondingID(s.ctx, ubd, unbondingID))
	err = s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.ErrorIs(err, types.ErrUnbondingOnHoldRefCountNegative)

	err = s.stakingKeeper.PutUnbondingOnHold(s.ctx, unbondingID)
	require.NoError(err)
	s.bankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(s.ctx, types.NotBondedPoolName, delAddrs[0], sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(5)))).Return(nil)
	err = s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.NoError(err)

	// redelegation
	unbondingID++
	require.NoError(s.stakingKeeper.SetUnbondingType(s.ctx, unbondingID, types.UnbondingType_Redelegation))
	err = s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.ErrorIs(err, types.ErrNoRedelegation)

	red := types.NewRedelegation(
		delAddrs[0],
		valAddrs[0],
		valAddrs[1],
		0,
		time.Unix(5, 0).UTC(),
		math.NewInt(10),
		math.LegacyNewDec(10),
		unbondingID,
		addresscodec.NewBech32Codec("cosmosvaloper"), addresscodec.NewBech32Codec("cosmos"),
	)
	require.NoError(s.stakingKeeper.SetRedelegation(s.ctx, red))
	require.NoError(s.stakingKeeper.SetRedelegationByUnbondingID(s.ctx, red, unbondingID))
	err = s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.ErrorIs(err, types.ErrUnbondingOnHoldRefCountNegative)

	require.NoError(s.stakingKeeper.PutUnbondingOnHold(s.ctx, unbondingID))
	require.NoError(s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID))

	// validator unbonding
	unbondingID++
	require.NoError(s.stakingKeeper.SetUnbondingType(s.ctx, unbondingID, types.UnbondingType_ValidatorUnbonding))
	err = s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.ErrorIs(err, types.ErrNoValidatorFound)

	val := testutil.NewValidator(s.T(), valAddrs[0], PKs[0])
	require.NoError(s.stakingKeeper.SetValidator(s.ctx, val))
	require.NoError(s.stakingKeeper.SetValidatorByUnbondingID(s.ctx, val, unbondingID))
	err = s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID)
	require.ErrorIs(err, types.ErrUnbondingOnHoldRefCountNegative)

	require.NoError(s.stakingKeeper.PutUnbondingOnHold(s.ctx, unbondingID))
	require.NoError(s.stakingKeeper.UnbondingCanComplete(s.ctx, unbondingID))
}
