package app_test

import (
	"strconv"
	"testing"
	"time"

	sdkmath "cosmossdk.io/math"
	"github.com/0glabs/0g-chain/app"
	evmutiltypes "github.com/0glabs/0g-chain/x/evmutil/types"
	precisebankkeeper "github.com/0glabs/0g-chain/x/precisebank/keeper"
	precisebanktypes "github.com/0glabs/0g-chain/x/precisebank/types"
	tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/stretchr/testify/require"
)

func TestMigrateEvmutilToPrecisebank(t *testing.T) {
	// Full test case with all components together
	tests := []struct {
		name               string
		initialReserve     sdkmath.Int
		fractionalBalances []sdkmath.Int
	}{
		{
			"no fractional balances",
			sdkmath.NewInt(0),
			[]sdkmath.Int{},
		},
		{
			"sufficient reserve, 0 remainder",
			// Accounts adding up to 2 int units, same as reserve
			sdkmath.NewInt(2),
			[]sdkmath.Int{
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"insufficient reserve, 0 remainder",
			// Accounts adding up to 2 int units, but only 1 int unit in reserve
			sdkmath.NewInt(1),
			[]sdkmath.Int{
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"excess reserve, 0 remainder",
			// Accounts adding up to 2 int units, but 3 int unit in reserve
			sdkmath.NewInt(3),
			[]sdkmath.Int{
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"sufficient reserve, non-zero remainder",
			// Accounts adding up to 1.5 int units, same as reserve
			sdkmath.NewInt(2),
			[]sdkmath.Int{
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"insufficient reserve, non-zero remainder",
			// Accounts adding up to 1.5 int units, less than reserve,
			// Reserve should be 2 and remainder 0.5
			sdkmath.NewInt(1),
			[]sdkmath.Int{
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"excess reserve, non-zero remainder",
			// Accounts adding up to 1.5 int units, 3 int units in reserve
			sdkmath.NewInt(3),
			[]sdkmath.Int{
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tApp := app.NewTestApp()
			tApp.InitializeFromGenesisStates()
			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()})

			ak := tApp.GetAccountKeeper()
			bk := tApp.GetBankKeeper()
			evmuk := tApp.GetEvmutilKeeper()
			pbk := tApp.GetPrecisebankKeeper()

			reserveCoin := sdk.NewCoin(precisebanktypes.IntegerCoinDenom, tt.initialReserve)
			err := bk.MintCoins(ctx, evmutiltypes.ModuleName, sdk.NewCoins(reserveCoin))
			require.NoError(t, err)

			oldReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(evmutiltypes.ModuleName)
			newReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(precisebanktypes.ModuleName)

			// Double check balances
			oldReserveBalance := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom)
			newReserveBalance := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom)

			require.Equal(t, tt.initialReserve, oldReserveBalance.Amount, "initial x/evmutil reserve balance")
			require.True(t, newReserveBalance.IsZero(), "empty initial new reserve")

			// Set accounts
			for i, balance := range tt.fractionalBalances {
				addr := sdk.AccAddress([]byte(strconv.Itoa(i)))

				err := evmuk.SetBalance(ctx, addr, balance)
				require.NoError(t, err)
			}

			// Run full x/evmutil -> x/precisebank migration
			err = app.MigrateEvmutilToPrecisebank(
				ctx,
				ak,
				bk,
				evmuk,
				pbk,
			)
			require.NoError(t, err)

			// Check old reserve is empty
			oldReserveBalanceAfter := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom)
			require.True(t, oldReserveBalanceAfter.IsZero(), "old reserve should be empty")

			// Check new reserve fully backs fractional balances
			newReserveBalanceAfter := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom)
			fractionalBalanceTotal := pbk.GetTotalSumFractionalBalances(ctx)
			remainder := pbk.GetRemainderAmount(ctx)

			expectedReserveBal := fractionalBalanceTotal.Add(remainder)
			require.Equal(
				t,
				expectedReserveBal,
				newReserveBalanceAfter.Amount.Mul(precisebanktypes.ConversionFactor()),
				"new reserve should equal total fractional balances",
			)

			// Check balances are deleted in evmutil and migrated to precisebank
			for i := range tt.fractionalBalances {
				addr := sdk.AccAddress([]byte(strconv.Itoa(i)))
				acc := evmuk.GetAccount(ctx, addr)
				require.Nil(t, acc, "account should be deleted")

				balance := pbk.GetFractionalBalance(ctx, addr)
				require.Equal(t, tt.fractionalBalances[i], balance, "balance should be migrated")
			}

			// Checks balances valid and remainder
			res, stop := precisebankkeeper.AllInvariants(pbk)(ctx)
			require.Falsef(t, stop, "invariants should pass: %s", res)
		})
	}
}

func TestTransferFractionalBalances(t *testing.T) {
	tests := []struct {
		name               string
		fractionalBalances []sdkmath.Int
	}{
		{
			"no fractional balances",
			[]sdkmath.Int{},
		},
		{
			"balanced fractional balances",
			[]sdkmath.Int{
				// 4 accounts
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"unbalanced balances",
			[]sdkmath.Int{
				// 3 accounts
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tApp := app.NewTestApp()
			tApp.InitializeFromGenesisStates()
			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()})

			evmutilk := tApp.GetEvmutilKeeper()
			pbk := tApp.GetPrecisebankKeeper()

			for i, balance := range tt.fractionalBalances {
				addr := sdk.AccAddress([]byte(strconv.Itoa(i)))

				err := evmutilk.SetBalance(ctx, addr, balance)
				require.NoError(t, err)
			}

			// Run balance transfer
			aggregateSum, err := app.TransferFractionalBalances(
				ctx,
				evmutilk,
				pbk,
			)
			require.NoError(t, err)

			// Check balances are deleted in evmutil and migrated to precisebank
			sum := sdkmath.ZeroInt()
			for i := range tt.fractionalBalances {
				sum = sum.Add(tt.fractionalBalances[i])

				addr := sdk.AccAddress([]byte(strconv.Itoa(i)))
				acc := evmutilk.GetAccount(ctx, addr)
				require.Nil(t, acc, "account should be deleted")

				balance := pbk.GetFractionalBalance(ctx, addr)
				require.Equal(t, tt.fractionalBalances[i], balance, "balance should be migrated")
			}

			require.Equal(t, sum, aggregateSum, "aggregate sum should be correct")
		})
	}
}

func TestInitializeRemainder(t *testing.T) {
	tests := []struct {
		name             string
		giveAggregateSum sdkmath.Int
		wantRemainder    sdkmath.Int
	}{
		{
			"0 remainder, 1ukava",
			precisebanktypes.ConversionFactor(),
			sdkmath.NewInt(0),
		},
		{
			"0 remainder, multiple ukava",
			precisebanktypes.ConversionFactor().MulRaw(5),
			sdkmath.NewInt(0),
		},
		{
			"non-zero remainder, min",
			precisebanktypes.ConversionFactor().SubRaw(1),
			sdkmath.NewInt(1),
		},
		{
			"non-zero remainder, max",
			sdkmath.NewInt(1),
			precisebanktypes.ConversionFactor().SubRaw(1),
		},
		{
			"non-zero remainder, half",
			precisebanktypes.ConversionFactor().QuoRaw(2),
			precisebanktypes.ConversionFactor().QuoRaw(2),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tApp := app.NewTestApp()
			tApp.InitializeFromGenesisStates()

			pbk := tApp.GetPrecisebankKeeper()

			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()})

			remainder := app.InitializeRemainder(
				ctx,
				tApp.GetPrecisebankKeeper(),
				tt.giveAggregateSum,
			)
			require.Equal(t, tt.wantRemainder, remainder)

			// Check actual state
			remainderAfter := pbk.GetRemainderAmount(ctx)
			require.Equal(t, tt.wantRemainder, remainderAfter)

			// Not checking invariants here since it requires actual balance state
			aggregateSumWithRemainder := tt.giveAggregateSum.Add(remainder)
			require.True(
				t,
				aggregateSumWithRemainder.
					Mod(precisebanktypes.ConversionFactor()).
					IsZero(),
				"remainder + aggregate sum should be a multiple of the conversion factor",
			)
		})
	}
}

func TestTransferFractionalBalanceReserve(t *testing.T) {
	tests := []struct {
		name               string
		initialReserve     sdk.Coin
		fractionalBalances []sdkmath.Int
	}{
		{
			"balanced reserve, no remainder",
			sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(1)),
			[]sdkmath.Int{
				// 2 accounts
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"insufficient reserve",
			sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(1)),
			[]sdkmath.Int{
				// 4 accounts, total 2 int units
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"extra reserve funds",
			sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(2)),
			[]sdkmath.Int{
				// 2 accounts, total 1 int units
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"insufficient reserve, with remainder",
			sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(1)),
			[]sdkmath.Int{
				// 5 accounts, total 2.5 int units
				// Expected 3 int units in reserve, 0.5 remainder
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
		{
			"extra reserve funds, with remainder",
			sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(3)),
			[]sdkmath.Int{
				// 3 accounts, total 1.5 int units.
				// Expected 2 int units in reserve, 0.5 remainder
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
				precisebanktypes.ConversionFactor().QuoRaw(2),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tApp := app.NewTestApp()
			tApp.InitializeFromGenesisStates()
			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()})

			bk := tApp.GetBankKeeper()
			pbk := tApp.GetPrecisebankKeeper()
			err := bk.MintCoins(ctx, evmutiltypes.ModuleName, sdk.NewCoins(tt.initialReserve))
			require.NoError(t, err)

			oldReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(evmutiltypes.ModuleName)
			newReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(precisebanktypes.ModuleName)

			// Double check balances
			oldReserveBalance := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom)
			newReserveBalance := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom)

			require.Equal(t, tt.initialReserve, oldReserveBalance)
			require.True(t, newReserveBalance.IsZero(), "empty initial new reserve")

			for i, balance := range tt.fractionalBalances {
				addr := sdk.AccAddress([]byte{byte(i)})

				require.NotPanics(t, func() {
					pbk.SetFractionalBalance(ctx, addr, balance)
				}, "given fractional balances should be valid")
			}

			// Run reserve migration
			err = app.TransferFractionalBalanceReserve(
				ctx,
				tApp.GetAccountKeeper(),
				bk,
				tApp.GetPrecisebankKeeper(),
			)
			require.NoError(t, err)

			// Check old reserve is empty
			oldReserveBalanceAfter := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom)
			require.True(t, oldReserveBalanceAfter.IsZero(), "old reserve should be empty")

			// Check new reserve fully backs fractional balances
			newReserveBalanceAfter := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom)
			fractionalBalanceTotal := pbk.GetTotalSumFractionalBalances(ctx)

			expectedReserveBal := fractionalBalanceTotal.
				Quo(precisebanktypes.ConversionFactor())

			// Check if theres a remainder
			if fractionalBalanceTotal.Mod(precisebanktypes.ConversionFactor()).IsPositive() {
				expectedReserveBal = expectedReserveBal.Add(sdkmath.OneInt())
			}

			require.Equal(
				t,
				expectedReserveBal,
				newReserveBalanceAfter.Amount,
				"new reserve should equal total fractional balances + remainder",
			)
		})
	}
}