package evm

import (
	"testing"

	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/require"
	"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
	"github.com/tech/sendico/gateway/chain/storage/model"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)

func TestComputeGasTopUp_BalanceSufficient(t *testing.T) {
	network := ethNetwork(defaultPolicy())
	wallet := &model.ManagedWallet{}

	topUp, capHit, err := ComputeGasTopUp(network, wallet, ethMoney("5"), ethMoney("30"))
	require.NoError(t, err)
	require.Nil(t, topUp)
	require.False(t, capHit)
}

func TestComputeGasTopUp_BufferedRequired(t *testing.T) {
	network := ethNetwork(defaultPolicy())
	wallet := &model.ManagedWallet{}

	topUp, capHit, err := ComputeGasTopUp(network, wallet, ethMoney("50"), ethMoney("10"))
	require.NoError(t, err)
	require.NotNil(t, topUp)
	require.False(t, capHit)
	require.Equal(t, "46000000000000000000", topUp.GetAmount())
	require.Equal(t, "ETH", topUp.GetCurrency())
}

func TestComputeGasTopUp_MinBalanceBinding(t *testing.T) {
	network := ethNetwork(defaultPolicy())
	wallet := &model.ManagedWallet{}

	topUp, capHit, err := ComputeGasTopUp(network, wallet, ethMoney("5"), ethMoney("1"))
	require.NoError(t, err)
	require.NotNil(t, topUp)
	require.False(t, capHit)
	require.Equal(t, "19000000000000000000", topUp.GetAmount())
}

func TestComputeGasTopUp_RoundsUp(t *testing.T) {
	policy := shared.GasTopUpPolicy{
		Default: shared.GasTopUpRule{
			BufferPercent:    decimal.NewFromFloat(0),
			MinNativeBalance: decimal.NewFromFloat(0),
			RoundingUnit:     decimal.NewFromFloat(1),
			MaxTopUp:         decimal.NewFromFloat(100),
		},
	}
	network := ethNetwork(&policy)
	wallet := &model.ManagedWallet{}

	topUp, capHit, err := ComputeGasTopUp(network, wallet, ethMoney("1.1"), ethMoney("0"))
	require.NoError(t, err)
	require.NotNil(t, topUp)
	require.False(t, capHit)
	require.Equal(t, "2000000000000000000", topUp.GetAmount())
}

func TestComputeGasTopUp_CapHit(t *testing.T) {
	policy := shared.GasTopUpPolicy{
		Default: shared.GasTopUpRule{
			BufferPercent:    decimal.NewFromFloat(0),
			MinNativeBalance: decimal.NewFromFloat(0),
			RoundingUnit:     decimal.NewFromFloat(1),
			MaxTopUp:         decimal.NewFromFloat(10),
		},
	}
	network := ethNetwork(&policy)
	wallet := &model.ManagedWallet{}

	topUp, capHit, err := ComputeGasTopUp(network, wallet, ethMoney("100"), ethMoney("0"))
	require.NoError(t, err)
	require.NotNil(t, topUp)
	require.True(t, capHit)
	require.Equal(t, "10000000000000000000", topUp.GetAmount())
}

func TestComputeGasTopUp_MinBalanceWhenRequiredZero(t *testing.T) {
	network := ethNetwork(defaultPolicy())
	wallet := &model.ManagedWallet{}

	topUp, capHit, err := ComputeGasTopUp(network, wallet, ethMoney("0"), ethMoney("5"))
	require.NoError(t, err)
	require.NotNil(t, topUp)
	require.False(t, capHit)
	require.Equal(t, "15000000000000000000", topUp.GetAmount())
}

func TestComputeGasTopUp_ContractPolicyOverride(t *testing.T) {
	policy := shared.GasTopUpPolicy{
		Default: shared.GasTopUpRule{
			BufferPercent:    decimal.NewFromFloat(0.1),
			MinNativeBalance: decimal.NewFromFloat(10),
			RoundingUnit:     decimal.NewFromFloat(1),
			MaxTopUp:         decimal.NewFromFloat(100),
		},
		Contract: &shared.GasTopUpRule{
			BufferPercent:    decimal.NewFromFloat(0.5),
			MinNativeBalance: decimal.NewFromFloat(5),
			RoundingUnit:     decimal.NewFromFloat(1),
			MaxTopUp:         decimal.NewFromFloat(100),
		},
	}
	network := ethNetwork(&policy)
	wallet := &model.ManagedWallet{ContractAddress: "0xcontract"}

	topUp, capHit, err := ComputeGasTopUp(network, wallet, ethMoney("10"), ethMoney("0"))
	require.NoError(t, err)
	require.NotNil(t, topUp)
	require.False(t, capHit)
	require.Equal(t, "15000000000000000000", topUp.GetAmount())
}

func defaultPolicy() *shared.GasTopUpPolicy {
	return &shared.GasTopUpPolicy{
		Default: shared.GasTopUpRule{
			BufferPercent:    decimal.NewFromFloat(0.15),
			MinNativeBalance: decimal.NewFromFloat(20),
			RoundingUnit:     decimal.NewFromFloat(1),
			MaxTopUp:         decimal.NewFromFloat(500),
		},
	}
}

func ethNetwork(policy *shared.GasTopUpPolicy) shared.Network {
	return shared.Network{
		Name:           "ethereum_mainnet",
		NativeToken:    "ETH",
		GasTopUpPolicy: policy,
	}
}

func ethMoney(eth string) *moneyv1.Money {
	value, _ := decimal.NewFromString(eth)
	baseUnits := value.Mul(evmBaseUnitFactor).Truncate(0)
	return &moneyv1.Money{
		Currency: "ETH",
		Amount:   baseUnits.StringFixed(0),
	}
}
