package types_test

import (
	"testing"

	"github.com/stretchr/testify/require"

	"cosmossdk.io/math"

	"git.cw.tr/mukan-network/mukan-sdk/crypto/keys/ed25519"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	bank "git.cw.tr/mukan-network/mukan-sdk/x/bank/types"
)

func TestBalanceValidate(t *testing.T) {
	testCases := []struct {
		name    string
		balance bank.Balance
		expErr  bool
	}{
		{
			"valid balance",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
				Coins:   sdk.Coins{sdk.NewInt64Coin("uatom", 1)},
			},
			false,
		},
		{"empty balance", bank.Balance{}, true},
		{
			"nil balance coins",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
			},
			false,
		},
		{
			"dup coins",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
				Coins: sdk.Coins{
					sdk.NewInt64Coin("uatom", 1),
					sdk.NewInt64Coin("uatom", 1),
				},
			},
			true,
		},
		{
			"invalid coin denom",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
				Coins: sdk.Coins{
					sdk.Coin{Denom: "", Amount: math.OneInt()},
				},
			},
			true,
		},
		{
			"negative coin",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
				Coins: sdk.Coins{
					sdk.Coin{Denom: "uatom", Amount: math.NewInt(-1)},
				},
			},
			true,
		},
		{
			"0 value coin",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
				Coins: sdk.Coins{
					sdk.NewInt64Coin("atom", 0),
					sdk.NewInt64Coin("zatom", 2),
				},
			},
			true,
		},
		{
			"unsorted coins",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
				Coins: sdk.Coins{
					sdk.NewInt64Coin("atom", 2),
					sdk.NewInt64Coin("zatom", 2),
					sdk.NewInt64Coin("batom", 12),
				},
			},
			true,
		},
		{
			"valid sorted coins",
			bank.Balance{
				Address: "cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t",
				Coins: sdk.Coins{
					sdk.NewInt64Coin("atom", 2),
					sdk.NewInt64Coin("batom", 12),
					sdk.NewInt64Coin("zatom", 2),
				},
			},
			false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.balance.Validate()

			if tc.expErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestBalance_GetAddress(t *testing.T) {
	tests := []struct {
		name    string
		Address string
		err     bool
	}{
		{"empty address", "", true},
		{"malformed address", "invalid", true},
		{"valid address", "cosmos1vy0ga0klndqy92ceqehfkvgmn4t94eteq4hmqv", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			b := bank.Balance{Address: tt.Address}
			if !tt.err {
				require.Equal(t, b.GetAddress(), tt.Address)
			} else {
				require.False(t, len(b.GetAddress()) != 0 && b.GetAddress() != tt.Address)
			}
		})
	}
}

func TestSanitizeBalances(t *testing.T) {
	// 1. Generate balances
	tokens := sdk.TokensFromConsensusPower(81, sdk.DefaultPowerReduction)
	coin := sdk.NewCoin("benchcoin", tokens)
	coins := sdk.Coins{coin}
	addrs, _ := makeRandomAddressesAndPublicKeys(20)

	var balances []bank.Balance
	for _, addr := range addrs {
		balances = append(balances, bank.Balance{
			Address: addr.String(),
			Coins:   coins,
		})
	}
	// 2. Sort the values.
	sorted := bank.SanitizeGenesisBalances(balances)

	// 3. Compare and ensure that all the values are sorted in ascending order.
	// Invariant after sorting:
	//    a[i] <= a[i+1...n]
	for i := range sorted {
		ai := sorted[i]
		// Ensure that every single value that comes after i is less than it.
		for j := i + 1; j < len(sorted); j++ {
			aj := sorted[j]
			if ai.GetAddress() == aj.GetAddress() {
				t.Errorf("Balance(%d) > Balance(%d)", i, j)
			}
		}
	}
}

func makeRandomAddressesAndPublicKeys(n int) (accL []sdk.AccAddress, pkL []*ed25519.PubKey) {
	for range n {
		pk := ed25519.GenPrivKey().PubKey().(*ed25519.PubKey)
		pkL = append(pkL, pk)
		accL = append(accL, sdk.AccAddress(pk.Address()))
	}
	return accL, pkL
}

var sink, revert any

func BenchmarkSanitizeBalances500(b *testing.B) {
	benchmarkSanitizeBalances(b, 500)
}

func BenchmarkSanitizeBalances1000(b *testing.B) {
	benchmarkSanitizeBalances(b, 1000)
}

func benchmarkSanitizeBalances(b *testing.B, nAddresses int) {
	b.Helper()
	b.ReportAllocs()
	tokens := sdk.TokensFromConsensusPower(81, sdk.DefaultPowerReduction)
	coin := sdk.NewCoin("benchcoin", tokens)
	coins := sdk.Coins{coin}
	addrs, _ := makeRandomAddressesAndPublicKeys(nAddresses)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		var balances []bank.Balance
		for _, addr := range addrs {
			balances = append(balances, bank.Balance{
				Address: addr.String(),
				Coins:   coins,
			})
		}
		sink = bank.SanitizeGenesisBalances(balances)
	}
	if sink == nil {
		b.Fatal("Benchmark did not run")
	}
	sink = revert
}
