package keeper_test

import (
	"testing"

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

	"github.com/0glabs/0g-chain/x/evmutil/testutil"
	"github.com/0glabs/0g-chain/x/evmutil/types"
)

type keeperTestSuite struct {
	testutil.Suite
}

func (suite *keeperTestSuite) SetupTest() {
	suite.Suite.SetupTest()
}

func (suite *keeperTestSuite) TestGetAllAccounts() {
	tests := []struct {
		name        string
		expAccounts []types.Account
	}{
		{
			"no accounts",
			[]types.Account{},
		},
		{
			"with accounts",
			[]types.Account{
				{Address: suite.Addrs[0], Balance: sdkmath.NewInt(100)},
				{Address: suite.Addrs[1], Balance: sdkmath.NewInt(200)},
			},
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			suite.SetupTest()

			for _, account := range tt.expAccounts {
				suite.Keeper.SetBalance(suite.Ctx, account.Address, account.Balance)
			}

			accounts := suite.Suite.Keeper.GetAllAccounts(suite.Ctx)
			if len(tt.expAccounts) == 0 {
				suite.Require().Len(tt.expAccounts, 0)
			} else {
				suite.Require().Equal(tt.expAccounts, accounts)
			}
		})
	}
}

func (suite *keeperTestSuite) TestSetAccount_ZeroBalance() {
	existingAccount := types.Account{
		Address: suite.Addrs[0],
		Balance: sdkmath.NewInt(100),
	}
	err := suite.Keeper.SetAccount(suite.Ctx, existingAccount)
	suite.Require().NoError(err)
	err = suite.Keeper.SetAccount(suite.Ctx, types.Account{
		Address: suite.Addrs[0],
		Balance: sdk.ZeroInt(),
	})
	suite.Require().NoError(err)
	bal := suite.Keeper.GetBalance(suite.Ctx, suite.Addrs[0])
	suite.Require().Equal(sdk.ZeroInt(), bal)
	expAcct := suite.Keeper.GetAccount(suite.Ctx, suite.Addrs[0])
	suite.Require().Nil(expAcct)
}

func (suite *keeperTestSuite) TestSetAccount() {
	existingAccount := types.Account{
		Address: suite.Addrs[0],
		Balance: sdkmath.NewInt(100),
	}
	tests := []struct {
		name    string
		account types.Account
		success bool
	}{
		{
			"invalid address",
			types.Account{Address: nil, Balance: sdkmath.NewInt(100)},
			false,
		},
		{
			"invalid balance",
			types.Account{Address: suite.Addrs[0], Balance: sdkmath.NewInt(-100)},
			false,
		},
		{
			"empty account",
			types.Account{},
			false,
		},
		{
			"valid account",
			types.Account{Address: suite.Addrs[1], Balance: sdkmath.NewInt(100)},
			true,
		},
		{
			"replaces account",
			types.Account{Address: suite.Addrs[0], Balance: sdkmath.NewInt(50)},
			true,
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			suite.SetupTest()

			err := suite.Keeper.SetAccount(suite.Ctx, existingAccount)
			suite.Require().NoError(err)
			err = suite.Keeper.SetAccount(suite.Ctx, tt.account)
			if tt.success {
				suite.Require().NoError(err)
				expAcct := suite.Keeper.GetAccount(suite.Ctx, tt.account.Address)
				suite.Require().Equal(tt.account, *expAcct)
			} else {
				suite.Require().Error(err)
				suite.Require().Nil(suite.Keeper.GetAccount(suite.Ctx, suite.Addrs[1]))
			}
		})
	}
}

func (suite *keeperTestSuite) TestSendBalance() {
	startingSenderBal := sdkmath.NewInt(100)
	startingRecipientBal := sdkmath.NewInt(50)
	tests := []struct {
		name            string
		amt             sdkmath.Int
		expSenderBal    sdkmath.Int
		expRecipientBal sdkmath.Int
		success         bool
	}{
		{
			"fails when sending negative amount",
			sdkmath.NewInt(-5),
			sdk.ZeroInt(),
			sdk.ZeroInt(),
			false,
		},
		{
			"send zero amount",
			sdk.ZeroInt(),
			startingSenderBal,
			startingRecipientBal,
			true,
		},
		{
			"fails when sender does not have enough balance",
			sdkmath.NewInt(101),
			sdk.ZeroInt(),
			sdk.ZeroInt(),
			false,
		},
		{
			"send valid amount",
			sdkmath.NewInt(80),
			sdkmath.NewInt(20),
			sdkmath.NewInt(130),
			true,
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			suite.SetupTest()

			err := suite.Keeper.SetBalance(suite.Ctx, suite.Addrs[0], startingSenderBal)
			suite.Require().NoError(err)
			err = suite.Keeper.SetBalance(suite.Ctx, suite.Addrs[1], startingRecipientBal)
			suite.Require().NoError(err)

			err = suite.Keeper.SendBalance(suite.Ctx, suite.Addrs[0], suite.Addrs[1], tt.amt)
			if tt.success {
				suite.Require().NoError(err)
				suite.Require().Equal(tt.expSenderBal, suite.Keeper.GetBalance(suite.Ctx, suite.Addrs[0]))
				suite.Require().Equal(tt.expRecipientBal, suite.Keeper.GetBalance(suite.Ctx, suite.Addrs[1]))
			} else {
				suite.Require().Error(err)
			}
		})
	}
}

func (suite *keeperTestSuite) TestSetBalance() {
	existingAccount := types.Account{
		Address: suite.Addrs[0],
		Balance: sdkmath.NewInt(100),
	}
	tests := []struct {
		name    string
		address sdk.AccAddress
		balance sdkmath.Int
		success bool
	}{
		{
			"invalid balance",
			suite.Addrs[0],
			sdkmath.NewInt(-100),
			false,
		},
		{
			"set new account balance",
			suite.Addrs[1],
			sdkmath.NewInt(100),
			true,
		},
		{
			"replace account balance",
			suite.Addrs[0],
			sdkmath.NewInt(50),
			true,
		},
		{
			"invalid address",
			nil,
			sdkmath.NewInt(100),
			false,
		},
		{
			"zero balance",
			suite.Addrs[0],
			sdk.ZeroInt(),
			true,
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			suite.SetupTest()

			err := suite.Keeper.SetAccount(suite.Ctx, existingAccount)
			suite.Require().NoError(err)
			err = suite.Keeper.SetBalance(suite.Ctx, tt.address, tt.balance)
			if tt.success {
				suite.Require().NoError(err)
				expBal := suite.Keeper.GetBalance(suite.Ctx, tt.address)
				suite.Require().Equal(expBal, tt.balance)

				if tt.balance.IsZero() {
					account := suite.Keeper.GetAccount(suite.Ctx, tt.address)
					suite.Require().Nil(account)
				}
			} else {
				suite.Require().Error(err)
				expBal := suite.Keeper.GetBalance(suite.Ctx, existingAccount.Address)
				suite.Require().Equal(expBal, existingAccount.Balance)
			}
		})
	}
}

func (suite *keeperTestSuite) TestRemoveBalance() {
	existingAccount := types.Account{
		Address: suite.Addrs[0],
		Balance: sdkmath.NewInt(100),
	}
	tests := []struct {
		name    string
		amt     sdkmath.Int
		expBal  sdkmath.Int
		success bool
	}{
		{
			"fails if amount is negative",
			sdkmath.NewInt(-10),
			sdk.ZeroInt(),
			false,
		},
		{
			"remove zero amount",
			sdk.ZeroInt(),
			existingAccount.Balance,
			true,
		},
		{
			"not enough balance",
			sdkmath.NewInt(101),
			sdk.ZeroInt(),
			false,
		},
		{
			"remove full balance",
			sdkmath.NewInt(100),
			sdk.ZeroInt(),
			true,
		},
		{
			"remove some balance",
			sdkmath.NewInt(10),
			sdkmath.NewInt(90),
			true,
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			suite.SetupTest()

			err := suite.Keeper.SetAccount(suite.Ctx, existingAccount)
			suite.Require().NoError(err)
			err = suite.Keeper.RemoveBalance(suite.Ctx, existingAccount.Address, tt.amt)
			if tt.success {
				suite.Require().NoError(err)
				expBal := suite.Keeper.GetBalance(suite.Ctx, existingAccount.Address)
				suite.Require().Equal(expBal, tt.expBal)
			} else {
				suite.Require().Error(err)
				expBal := suite.Keeper.GetBalance(suite.Ctx, existingAccount.Address)
				suite.Require().Equal(expBal, existingAccount.Balance)
			}
		})
	}
}

func (suite *keeperTestSuite) TestGetBalance() {
	existingAccount := types.Account{
		Address: suite.Addrs[0],
		Balance: sdkmath.NewInt(100),
	}
	tests := []struct {
		name   string
		addr   sdk.AccAddress
		expBal sdkmath.Int
	}{
		{
			"returns 0 balance if account does not exist",
			suite.Addrs[1],
			sdk.ZeroInt(),
		},
		{
			"returns account balance",
			suite.Addrs[0],
			sdkmath.NewInt(100),
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			suite.SetupTest()

			err := suite.Keeper.SetAccount(suite.Ctx, existingAccount)
			suite.Require().NoError(err)
			balance := suite.Keeper.GetBalance(suite.Ctx, tt.addr)
			suite.Require().Equal(tt.expBal, balance)
		})
	}
}

func (suite *keeperTestSuite) TestDeployedCosmosCoinContractStoreState() {
	suite.Run("returns nil for nonexistent denom", func() {
		suite.SetupTest()
		addr, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, "undeployed-denom")
		suite.False(found)
		suite.Equal(addr, types.InternalEVMAddress{})
	})

	suite.Run("handles setting & getting a contract address", func() {
		suite.SetupTest()
		denom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"
		address := testutil.RandomInternalEVMAddress()

		err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, denom, address)
		suite.NoError(err)

		stored, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
		suite.True(found)
		suite.Equal(address, stored)
	})

	suite.Run("fails when setting an invalid denom", func() {
		suite.SetupTest()
		invalidDenom := ""
		err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, invalidDenom, testutil.RandomInternalEVMAddress())
		suite.ErrorContains(err, "invalid cosmos denom")
	})

	suite.Run("fails when setting 0 address", func() {
		suite.SetupTest()
		invalidAddr := types.InternalEVMAddress{}
		err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, "denom", invalidAddr)
		suite.ErrorContains(err, "attempting to register empty contract address")
	})
}

func (suite *keeperTestSuite) TestIterateAllDeployedCosmosCoinContracts() {
	suite.SetupTest()
	address := testutil.RandomInternalEVMAddress()
	denoms := []string{}
	register := func(denom string) {
		addr := testutil.RandomInternalEVMAddress()
		if denom == "waldo" {
			addr = address
		}
		err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, denom, addr)
		suite.NoError(err)
		denoms = append(denoms, denom)
	}

	// register some contracts
	register("magic")
	register("popcorn")
	register("waldo")
	register("zzz")
	register("ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2")

	suite.Run("stops when told", func() {
		// test out stopping the iteration
		// NOTE: don't actually look for a single contract this way. the keys are deterministic by denom.
		var contract types.DeployedCosmosCoinContract
		suite.Keeper.IterateAllDeployedCosmosCoinContracts(suite.Ctx, func(c types.DeployedCosmosCoinContract) bool {
			contract = c
			return c.CosmosDenom == "waldo"
		})
		suite.Equal(types.NewDeployedCosmosCoinContract("waldo", address), contract)
	})

	suite.Run("iterates all contracts", func() {
		foundDenoms := make([]string, 0, len(denoms))
		suite.Keeper.IterateAllDeployedCosmosCoinContracts(suite.Ctx, func(c types.DeployedCosmosCoinContract) bool {
			foundDenoms = append(foundDenoms, c.CosmosDenom)
			return false
		})
		suite.Len(foundDenoms, len(denoms))
		suite.ElementsMatch(denoms, foundDenoms)
	})
}

func TestKeeperTestSuite(t *testing.T) {
	suite.Run(t, new(keeperTestSuite))
}