package types

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestNewDecCoin(t *testing.T) {
	require.NotPanics(t, func() {
		NewInt64DecCoin(testDenom1, 5)
	})
	require.NotPanics(t, func() {
		NewInt64DecCoin(testDenom1, 0)
	})
	require.Panics(t, func() {
		NewInt64DecCoin(strings.ToUpper(testDenom1), 5)
	})
	require.Panics(t, func() {
		NewInt64DecCoin(testDenom1, -5)
	})
}

func TestNewDecCoinFromDec(t *testing.T) {
	require.NotPanics(t, func() {
		NewDecCoinFromDec(testDenom1, NewDec(5))
	})
	require.NotPanics(t, func() {
		NewDecCoinFromDec(testDenom1, ZeroDec())
	})
	require.Panics(t, func() {
		NewDecCoinFromDec(strings.ToUpper(testDenom1), NewDec(5))
	})
	require.Panics(t, func() {
		NewDecCoinFromDec(testDenom1, NewDec(-5))
	})
}

func TestNewDecCoinFromCoin(t *testing.T) {
	require.NotPanics(t, func() {
		NewDecCoinFromCoin(Coin{testDenom1, NewInt(5)})
	})
	require.NotPanics(t, func() {
		NewDecCoinFromCoin(Coin{testDenom1, NewInt(0)})
	})
	require.Panics(t, func() {
		NewDecCoinFromCoin(Coin{strings.ToUpper(testDenom1), NewInt(5)})
	})
	require.Panics(t, func() {
		NewDecCoinFromCoin(Coin{testDenom1, NewInt(-5)})
	})
}

func TestDecCoinIsPositive(t *testing.T) {
	dc := NewInt64DecCoin(testDenom1, 5)
	require.True(t, dc.IsPositive())

	dc = NewInt64DecCoin(testDenom1, 0)
	require.False(t, dc.IsPositive())
}

func TestAddDecCoin(t *testing.T) {
	decCoinA1 := NewDecCoinFromDec(testDenom1, NewDecWithPrec(11, 1))
	decCoinA2 := NewDecCoinFromDec(testDenom1, NewDecWithPrec(22, 1))
	decCoinB1 := NewDecCoinFromDec(testDenom2, NewDecWithPrec(11, 1))

	// regular add
	res := decCoinA1.Add(decCoinA1)
	require.Equal(t, decCoinA2, res, "sum of coins is incorrect")

	// bad denom add
	require.Panics(t, func() {
		decCoinA1.Add(decCoinB1)
	}, "expected panic on sum of different denoms")
}

func TestAddDecCoins(t *testing.T) {
	one := NewDec(1)
	zero := NewDec(0)
	two := NewDec(2)

	cases := []struct {
		inputOne DecCoins
		inputTwo DecCoins
		expected DecCoins
	}{
		{DecCoins{{testDenom1, one}, {testDenom2, one}}, DecCoins{{testDenom1, one}, {testDenom2, one}}, DecCoins{{testDenom1, two}, {testDenom2, two}}},
		{DecCoins{{testDenom1, zero}, {testDenom2, one}}, DecCoins{{testDenom1, zero}, {testDenom2, zero}}, DecCoins{{testDenom2, one}}},
		{DecCoins{{testDenom1, zero}, {testDenom2, zero}}, DecCoins{{testDenom1, zero}, {testDenom2, zero}}, DecCoins(nil)},
	}

	for tcIndex, tc := range cases {
		res := tc.inputOne.Add(tc.inputTwo...)
		require.Equal(t, tc.expected, res, "sum of coins is incorrect, tc #%d", tcIndex)
	}
}

func TestIsValid(t *testing.T) {
	tests := []struct {
		coin       DecCoin
		expectPass bool
		msg        string
	}{
		{
			NewDecCoin("mytoken", NewInt(10)),
			true,
			"valid coins should have passed",
		},
		{
			DecCoin{Denom: "BTC", Amount: NewDec(10)},
			false,
			"invalid denoms",
		},
		{
			DecCoin{Denom: "BTC", Amount: NewDec(-10)},
			false,
			"negative amount",
		},
	}

	for _, tc := range tests {
		tc := tc
		if tc.expectPass {
			require.True(t, tc.coin.IsValid(), tc.msg)
		} else {
			require.False(t, tc.coin.IsValid(), tc.msg)
		}
	}
}

func TestSubDecCoin(t *testing.T) {
	tests := []struct {
		coin       DecCoin
		expectPass bool
		msg        string
	}{
		{
			NewDecCoin("mytoken", NewInt(20)),
			true,
			"valid coins should have passed",
		},
		{
			NewDecCoin("othertoken", NewInt(20)),
			false,
			"denom mismatch",
		},
		{
			NewDecCoin("mytoken", NewInt(9)),
			false,
			"negative amount",
		},
	}

	decCoin := NewDecCoin("mytoken", NewInt(10))

	for _, tc := range tests {
		tc := tc
		if tc.expectPass {
			equal := tc.coin.Sub(decCoin)
			require.Equal(t, equal, decCoin, tc.msg)
		} else {
			require.Panics(t, func() { tc.coin.Sub(decCoin) }, tc.msg)
		}
	}
}

func TestSubDecCoins(t *testing.T) {
	tests := []struct {
		coins      DecCoins
		expectPass bool
		msg        string
	}{
		{
			NewDecCoinsFromCoins(NewCoin("mytoken", NewInt(10)), NewCoin("btc", NewInt(20)), NewCoin("eth", NewInt(30))),
			true,
			"sorted coins should have passed",
		},
		{
			DecCoins{NewDecCoin("mytoken", NewInt(10)), NewDecCoin("btc", NewInt(20)), NewDecCoin("eth", NewInt(30))},
			false,
			"unorted coins should panic",
		},
		{
			DecCoins{DecCoin{Denom: "BTC", Amount: NewDec(10)}, NewDecCoin("eth", NewInt(15)), NewDecCoin("mytoken", NewInt(5))},
			false,
			"invalid denoms",
		},
	}

	decCoins := NewDecCoinsFromCoins(NewCoin("btc", NewInt(10)), NewCoin("eth", NewInt(15)), NewCoin("mytoken", NewInt(5)))

	for _, tc := range tests {
		tc := tc
		if tc.expectPass {
			equal := tc.coins.Sub(decCoins)
			require.Equal(t, equal, decCoins, tc.msg)
		} else {
			require.Panics(t, func() { tc.coins.Sub(decCoins) }, tc.msg)
		}
	}
}

func TestSortDecCoins(t *testing.T) {
	good := DecCoins{
		NewInt64DecCoin("gas", 1),
		NewInt64DecCoin("mineral", 1),
		NewInt64DecCoin("tree", 1),
	}
	empty := DecCoins{
		NewInt64DecCoin("gold", 0),
	}
	badSort1 := DecCoins{
		NewInt64DecCoin("tree", 1),
		NewInt64DecCoin("gas", 1),
		NewInt64DecCoin("mineral", 1),
	}
	badSort2 := DecCoins{ // both are after the first one, but the second and third are in the wrong order
		NewInt64DecCoin("gas", 1),
		NewInt64DecCoin("tree", 1),
		NewInt64DecCoin("mineral", 1),
	}
	badAmt := DecCoins{
		NewInt64DecCoin("gas", 1),
		NewInt64DecCoin("tree", 0),
		NewInt64DecCoin("mineral", 1),
	}
	dup := DecCoins{
		NewInt64DecCoin("gas", 1),
		NewInt64DecCoin("gas", 1),
		NewInt64DecCoin("mineral", 1),
	}

	cases := []struct {
		coins         DecCoins
		before, after bool // valid before/after sort
	}{
		{good, true, true},
		{empty, false, false},
		{badSort1, false, true},
		{badSort2, false, true},
		{badAmt, false, false},
		{dup, false, false},
	}

	for tcIndex, tc := range cases {
		require.Equal(t, tc.before, tc.coins.IsValid(), "coin validity is incorrect before sorting, tc #%d", tcIndex)
		tc.coins.Sort()
		require.Equal(t, tc.after, tc.coins.IsValid(), "coin validity is incorrect after sorting, tc #%d", tcIndex)
	}
}

func TestDecCoinsIsValid(t *testing.T) {
	testCases := []struct {
		input    DecCoins
		expected bool
	}{
		{DecCoins{}, true},
		{DecCoins{DecCoin{testDenom1, NewDec(5)}}, true},
		{DecCoins{DecCoin{testDenom1, NewDec(5)}, DecCoin{testDenom2, NewDec(100000)}}, true},
		{DecCoins{DecCoin{testDenom1, NewDec(-5)}}, false},
		{DecCoins{DecCoin{"AAA", NewDec(5)}}, false},
		{DecCoins{DecCoin{testDenom1, NewDec(5)}, DecCoin{"B", NewDec(100000)}}, false},
		{DecCoins{DecCoin{testDenom1, NewDec(5)}, DecCoin{testDenom2, NewDec(-100000)}}, false},
		{DecCoins{DecCoin{testDenom1, NewDec(-5)}, DecCoin{testDenom2, NewDec(100000)}}, false},
		{DecCoins{DecCoin{"AAA", NewDec(5)}, DecCoin{testDenom2, NewDec(100000)}}, false},
	}

	for i, tc := range testCases {
		res := tc.input.IsValid()
		require.Equal(t, tc.expected, res, "unexpected result for test case #%d, input: %v", i, tc.input)
	}
}

func TestParseDecCoins(t *testing.T) {
	testCases := []struct {
		input          string
		expectedResult DecCoins
		expectedErr    bool
	}{
		{"", nil, false},
		{"4stake", nil, true},
		{"5.5atom,4stake", nil, true},
		{"0.0stake", nil, true},
		{"0.004STAKE", nil, true},
		{
			"0.004stake",
			DecCoins{NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision))},
			false,
		},
		{
			"5.04atom,0.004stake",
			DecCoins{
				NewDecCoinFromDec("atom", NewDecWithPrec(5040000000000000000, Precision)),
				NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision)),
			},
			false,
		},
	}

	for i, tc := range testCases {
		res, err := ParseDecCoins(tc.input)
		if tc.expectedErr {
			require.Error(t, err, "expected error for test case #%d, input: %v", i, tc.input)
		} else {
			require.NoError(t, err, "unexpected error for test case #%d, input: %v", i, tc.input)
			require.Equal(t, tc.expectedResult, res, "unexpected result for test case #%d, input: %v", i, tc.input)
		}
	}
}

func TestDecCoinsString(t *testing.T) {
	testCases := []struct {
		input    DecCoins
		expected string
	}{
		{DecCoins{}, ""},
		{
			DecCoins{
				NewDecCoinFromDec("atom", NewDecWithPrec(5040000000000000000, Precision)),
				NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision)),
			},
			"5.040000000000000000atom,0.004000000000000000stake",
		},
	}

	for i, tc := range testCases {
		out := tc.input.String()
		require.Equal(t, tc.expected, out, "unexpected result for test case #%d, input: %v", i, tc.input)
	}
}

func TestDecCoinsIntersect(t *testing.T) {
	testCases := []struct {
		input1         string
		input2         string
		expectedResult string
	}{
		{"", "", ""},
		{"1.0stake", "", ""},
		{"1.0stake", "1.0stake", "1.0stake"},
		{"", "1.0stake", ""},
		{"1.0stake", "", ""},
		{"2.0stake,1.0trope", "1.9stake", "1.9stake"},
		{"2.0stake,1.0trope", "2.1stake", "2.0stake"},
		{"2.0stake,1.0trope", "0.9trope", "0.9trope"},
		{"2.0stake,1.0trope", "1.9stake,0.9trope", "1.9stake,0.9trope"},
		{"2.0stake,1.0trope", "1.9stake,0.9trope,20.0other", "1.9stake,0.9trope"},
		{"2.0stake,1.0trope", "1.0other", ""},
	}

	for i, tc := range testCases {
		in1, err := ParseDecCoins(tc.input1)
		require.NoError(t, err, "unexpected parse error in %v", i)
		in2, err := ParseDecCoins(tc.input2)
		require.NoError(t, err, "unexpected parse error in %v", i)
		exr, err := ParseDecCoins(tc.expectedResult)
		require.NoError(t, err, "unexpected parse error in %v", i)

		require.True(t, in1.Intersect(in2).IsEqual(exr), "in1.cap(in2) != exr in %v", i)
	}
}

func TestDecCoinsTruncateDecimal(t *testing.T) {
	decCoinA := NewDecCoinFromDec("bar", MustNewDecFromStr("5.41"))
	decCoinB := NewDecCoinFromDec("foo", MustNewDecFromStr("6.00"))

	testCases := []struct {
		input          DecCoins
		truncatedCoins Coins
		changeCoins    DecCoins
	}{
		{DecCoins{}, Coins(nil), DecCoins(nil)},
		{
			DecCoins{decCoinA, decCoinB},
			Coins{NewInt64Coin(decCoinA.Denom, 5), NewInt64Coin(decCoinB.Denom, 6)},
			DecCoins{NewDecCoinFromDec(decCoinA.Denom, MustNewDecFromStr("0.41"))},
		},
		{
			DecCoins{decCoinB},
			Coins{NewInt64Coin(decCoinB.Denom, 6)},
			DecCoins(nil),
		},
	}

	for i, tc := range testCases {
		truncatedCoins, changeCoins := tc.input.TruncateDecimal()
		require.Equal(
			t, tc.truncatedCoins, truncatedCoins,
			"unexpected truncated coins; tc #%d, input: %s", i, tc.input,
		)
		require.Equal(
			t, tc.changeCoins, changeCoins,
			"unexpected change coins; tc #%d, input: %s", i, tc.input,
		)
	}
}

func TestDecCoinsQuoDecTruncate(t *testing.T) {
	x := MustNewDecFromStr("1.00")
	y := MustNewDecFromStr("10000000000000000000.00")

	testCases := []struct {
		coins  DecCoins
		input  Dec
		result DecCoins
		panics bool
	}{
		{DecCoins{}, ZeroDec(), DecCoins(nil), true},
		{DecCoins{NewDecCoinFromDec("foo", x)}, y, DecCoins(nil), false},
		{DecCoins{NewInt64DecCoin("foo", 5)}, NewDec(2), DecCoins{NewDecCoinFromDec("foo", MustNewDecFromStr("2.5"))}, false},
	}

	for i, tc := range testCases {
		tc := tc
		if tc.panics {
			require.Panics(t, func() { tc.coins.QuoDecTruncate(tc.input) })
		} else {
			res := tc.coins.QuoDecTruncate(tc.input)
			require.Equal(t, tc.result, res, "unexpected result; tc #%d, coins: %s, input: %s", i, tc.coins, tc.input)
		}
	}
}

func TestNewDecCoinsWithIsValid(t *testing.T) {
	fake1 := append(NewDecCoins(NewDecCoin("mytoken", NewInt(10))), DecCoin{Denom: "BTC", Amount: NewDec(10)})
	fake2 := append(NewDecCoins(NewDecCoin("mytoken", NewInt(10))), DecCoin{Denom: "BTC", Amount: NewDec(-10)})

	tests := []struct {
		coin       DecCoins
		expectPass bool
		msg        string
	}{
		{
			NewDecCoins(NewDecCoin("mytoken", NewInt(10))),
			true,
			"valid coins should have passed",
		},
		{
			fake1,
			false,
			"invalid denoms",
		},
		{
			fake2,
			false,
			"negative amount",
		},
	}

	for _, tc := range tests {
		tc := tc
		if tc.expectPass {
			require.True(t, tc.coin.IsValid(), tc.msg)
		} else {
			require.False(t, tc.coin.IsValid(), tc.msg)
		}
	}
}

func TestDecCoins_AddDecCoinWithIsValid(t *testing.T) {
	lengthTestDecCoins := NewDecCoins().Add(NewDecCoin("mytoken", NewInt(10))).Add(DecCoin{Denom: "BTC", Amount: NewDec(10)})
	require.Equal(t, 2, len(lengthTestDecCoins), "should be 2")

	tests := []struct {
		coin       DecCoins
		expectPass bool
		msg        string
	}{
		{
			NewDecCoins().Add(NewDecCoin("mytoken", NewInt(10))),
			true,
			"valid coins should have passed",
		},
		{
			NewDecCoins().Add(NewDecCoin("mytoken", NewInt(10))).Add(DecCoin{Denom: "BTC", Amount: NewDec(10)}),
			false,
			"invalid denoms",
		},
		{
			NewDecCoins().Add(NewDecCoin("mytoken", NewInt(10))).Add(DecCoin{Denom: "BTC", Amount: NewDec(-10)}),
			false,
			"negative amount",
		},
	}

	for _, tc := range tests {
		tc := tc
		if tc.expectPass {
			require.True(t, tc.coin.IsValid(), tc.msg)
		} else {
			require.False(t, tc.coin.IsValid(), tc.msg)
		}
	}
}
