package params

import (
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math"
	"math/big"
	"strings"
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/address"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutusrpc"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestParam_UnmarshalJSON(t *testing.T) {
	type testCase struct {
		check              func(t *testing.T, p *Param)
		expectedRawMessage []byte
	}
	msg := `["123", 123, null, ["str2", 3], [{"type": "String", "value": "jajaja"}],
	  {"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"},
	  {"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"},
	  [{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}]]`
	expected := []testCase{
		{
			check: func(t *testing.T, p *Param) {
				expectedS := "123"
				actualS, err := p.GetStringStrict()
				require.NoError(t, err)
				require.Equal(t, expectedS, actualS)
				actualS, err = p.GetString()
				require.NoError(t, err)
				require.Equal(t, expectedS, actualS)

				expectedI := 123
				_, err = p.GetIntStrict()
				require.Error(t, err)
				actualI, err := p.GetInt()
				require.NoError(t, err)
				require.Equal(t, expectedI, actualI)

				expectedB := true
				_, err = p.GetBooleanStrict()
				require.Error(t, err)
				actualB, err := p.GetBoolean()
				require.NoError(t, err)
				require.Equal(t, expectedB, actualB)

				_, err = p.GetArray()
				require.Error(t, err)
			},
			expectedRawMessage: []byte(`"123"`),
		},
		{
			check: func(t *testing.T, p *Param) {
				expectedS := "123"
				_, err := p.GetStringStrict()
				require.Error(t, err)
				actualS, err := p.GetString()
				require.NoError(t, err)
				require.Equal(t, expectedS, actualS)

				expectedI := 123
				actualI, err := p.GetIntStrict()
				require.NoError(t, err)
				require.Equal(t, expectedI, actualI)
				actualI, err = p.GetInt()
				require.NoError(t, err)
				require.Equal(t, expectedI, actualI)

				expectedB := true
				_, err = p.GetBooleanStrict()
				require.Error(t, err)
				actualB, err := p.GetBoolean()
				require.NoError(t, err)
				require.Equal(t, expectedB, actualB)

				_, err = p.GetArray()
				require.Error(t, err)
			},
			expectedRawMessage: []byte(`123`),
		},
		{
			check: func(t *testing.T, p *Param) {
				_, err := p.GetStringStrict()
				require.Error(t, err)
				_, err = p.GetString()
				require.Error(t, err)

				_, err = p.GetIntStrict()
				require.Error(t, err)
				_, err = p.GetInt()
				require.Error(t, err)

				_, err = p.GetBooleanStrict()
				require.Error(t, err)
				_, err = p.GetBoolean()
				require.Error(t, err)

				_, err = p.GetArray()
				require.Error(t, err)
			},
			expectedRawMessage: []byte(`null`),
		},
		{
			check: func(t *testing.T, p *Param) {
				_, err := p.GetStringStrict()
				require.Error(t, err)
				_, err = p.GetString()
				require.Error(t, err)

				_, err = p.GetIntStrict()
				require.Error(t, err)
				_, err = p.GetInt()
				require.Error(t, err)

				_, err = p.GetBooleanStrict()
				require.Error(t, err)
				_, err = p.GetBoolean()
				require.Error(t, err)

				a, err := p.GetArray()
				require.NoError(t, err)
				require.Equal(t, []Param{
					{RawMessage: json.RawMessage(`"str2"`)},
					{RawMessage: json.RawMessage(`3`)},
				}, a)
			},
			expectedRawMessage: []byte(`["str2", 3]`),
		},
		{
			check: func(t *testing.T, p *Param) {
				a, err := p.GetArray()
				require.NoError(t, err)
				require.Equal(t, 1, len(a))
				fp, err := a[0].GetFuncParam()
				require.NoError(t, err)
				require.Equal(t, smartcontract.StringType, fp.Type)
				strVal, err := fp.Value.GetStringStrict()
				require.NoError(t, err)
				require.Equal(t, "jajaja", strVal)
			},
			expectedRawMessage: []byte(`[{"type": "String", "value": "jajaja"}]`),
		},
		{
			check: func(t *testing.T, p *Param) {
				actual, err := p.GetSignerWithWitness()
				require.NoError(t, err)
				expectedAcc, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
				require.NoError(t, err)
				require.Equal(t, tutusrpc.SignerWithWitness{Signer: transaction.Signer{Account: expectedAcc}}, actual)
			},
			expectedRawMessage: []byte(`{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}`),
		},
		{
			check: func(t *testing.T, p *Param) {
				actual, err := p.GetSignerWithWitness()
				require.NoError(t, err)
				expectedAcc, err := address.StringToUint160("NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag")
				require.NoError(t, err)
				require.Equal(t, tutusrpc.SignerWithWitness{Signer: transaction.Signer{Account: expectedAcc, Scopes: transaction.Global}}, actual)
			},
			expectedRawMessage: []byte(`{"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"}`),
		},
		{
			check: func(t *testing.T, p *Param) {
				actualSigs, actualWtns, err := p.GetSignersWithWitnesses()
				require.NoError(t, err)
				require.Equal(t, 1, len(actualSigs))
				require.Equal(t, 1, len(actualWtns))
				expectedAcc, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
				require.NoError(t, err)
				require.Equal(t, transaction.Signer{Account: expectedAcc, Scopes: transaction.Global}, actualSigs[0])
				require.Equal(t, transaction.Witness{}, actualWtns[0])
			},
			expectedRawMessage: []byte(`[{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}]`),
		},
	}

	var ps Params
	require.NoError(t, json.Unmarshal([]byte(msg), &ps))
	require.Equal(t, len(expected), len(ps))
	for i, tc := range expected {
		require.NotNil(t, ps[i])
		require.Equal(t, json.RawMessage(tc.expectedRawMessage), ps[i].RawMessage, i)
		tc.check(t, &ps[i])
	}
}

func TestGetBigInt(t *testing.T) {
	maxUint64 := new(big.Int).SetUint64(math.MaxUint64)
	minInt64 := big.NewInt(math.MinInt64)
	testCases := []struct {
		raw      string
		expected *big.Int
	}{
		{"true", big.NewInt(1)},
		{"false", new(big.Int)},
		{"42", big.NewInt(42)},
		{`"` + minInt64.String() + `"`, minInt64},
		{`"` + maxUint64.String() + `"`, maxUint64},
		{`"` + minInt64.String() + `000"`, new(big.Int).Mul(minInt64, big.NewInt(1000))},
		{`"` + maxUint64.String() + `000"`, new(big.Int).Mul(maxUint64, big.NewInt(1000))},
		{`"abc"`, nil},
		{`[]`, nil},
		{`null`, nil},
	}

	for _, tc := range testCases {
		var p Param
		require.NoError(t, json.Unmarshal([]byte(tc.raw), &p))

		actual, err := p.GetBigInt()
		if tc.expected == nil {
			require.Error(t, err)
			continue
		}
		require.NoError(t, err)
		require.Equal(t, tc.expected, actual)

		expected := tc.expected.Int64()
		actualInt, err := p.GetInt()
		if !actual.IsInt64() || int64(int(expected)) != expected {
			require.Error(t, err)
		} else {
			require.NoError(t, err)
			require.Equal(t, int(expected), actualInt)
		}
	}
}

func TestGetWitness(t *testing.T) {
	accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
	require.NoError(t, err)
	addrHash, err := address.StringToUint160("NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag")
	require.NoError(t, err)

	testCases := []struct {
		raw        string
		expected   tutusrpc.SignerWithWitness
		shouldFail bool
	}{
		{
			raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}`,
			expected: tutusrpc.SignerWithWitness{
				Signer: transaction.Signer{
					Account: accountHash,
					Scopes:  transaction.None,
				},
			},
		},
		{
			raw: `{"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"}`,
			expected: tutusrpc.SignerWithWitness{
				Signer: transaction.Signer{
					Account: addrHash,
					Scopes:  transaction.Global,
				},
			},
		},
		{
			raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}`,
			expected: tutusrpc.SignerWithWitness{
				Signer: transaction.Signer{
					Account: accountHash,
					Scopes:  transaction.Global,
				},
			},
		},
		{
			raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 128}`,
			expected: tutusrpc.SignerWithWitness{
				Signer: transaction.Signer{
					Account: accountHash,
					Scopes:  transaction.Global,
				},
			},
		},
		{
			raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}`,
			expected: tutusrpc.SignerWithWitness{
				Signer: transaction.Signer{
					Account: accountHash,
					Scopes:  transaction.None,
				},
			},
		},
		{
			raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 1}`,
			expected: tutusrpc.SignerWithWitness{
				Signer: transaction.Signer{
					Account: accountHash,
					Scopes:  transaction.CalledByEntry,
				},
			},
		},
		{
			raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 17}`,
			expected: tutusrpc.SignerWithWitness{
				Signer: transaction.Signer{
					Account: accountHash,
					Scopes:  transaction.CalledByEntry | transaction.CustomContracts,
				},
			},
		},
		{
			raw:        `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 178}`,
			shouldFail: true,
		},
		{
			raw:        `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 2}`,
			shouldFail: true,
		},
	}

	for _, tc := range testCases {
		p := Param{RawMessage: json.RawMessage(tc.raw)}
		actual, err := p.GetSignerWithWitness()
		if tc.shouldFail {
			require.Error(t, err, tc.raw)
		} else {
			require.NoError(t, err, tc.raw)
			require.Equal(t, tc.expected, actual)

			actual, err = p.GetSignerWithWitness() // valid second invocation.
			require.NoError(t, err, tc.raw)
			require.Equal(t, tc.expected, actual)
		}
	}
}

func TestParamGetUint256(t *testing.T) {
	gas := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"
	u256, _ := util.Uint256DecodeStringLE(gas)
	p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, gas))}
	u, err := p.GetUint256()
	assert.Equal(t, u256, u)
	require.Nil(t, err)

	p = Param{RawMessage: []byte(fmt.Sprintf(`"0x%s"`, gas))}
	u, err = p.GetUint256()
	require.NoError(t, err)
	assert.Equal(t, u256, u)

	p = Param{RawMessage: []byte(`42`)}
	_, err = p.GetUint256()
	require.NotNil(t, err)

	p = Param{RawMessage: []byte(`"qq2c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"`)}
	_, err = p.GetUint256()
	require.NotNil(t, err)
}

func TestParamGetUint160FromHex(t *testing.T) {
	in := "50befd26fdf6e4d957c11e078b24ebce6291456f"
	u160, _ := util.Uint160DecodeStringLE(in)
	p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, in))}
	u, err := p.GetUint160FromHex()
	assert.Equal(t, u160, u)
	require.Nil(t, err)

	p = Param{RawMessage: []byte(`42`)}
	_, err = p.GetUint160FromHex()
	require.NotNil(t, err)

	p = Param{RawMessage: []byte(`"wwbefd26fdf6e4d957c11e078b24ebce6291456f"`)}
	_, err = p.GetUint160FromHex()
	require.NotNil(t, err)
}

func TestParamGetUint160FromAddress(t *testing.T) {
	in := "NPAsqZkx9WhNd4P72uhZxBhLinSuNkxfB8"
	u160, _ := address.StringToUint160(in)
	p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, in))}
	u, err := p.GetUint160FromAddress()
	assert.Equal(t, u160, u)
	require.Nil(t, err)

	p = Param{RawMessage: []byte(`42`)}
	_, err = p.GetUint160FromAddress()
	require.NotNil(t, err)

	p = Param{RawMessage: []byte(`"QK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"`)}
	_, err = p.GetUint160FromAddress()
	require.NotNil(t, err)
}

func TestParam_GetUint160FromAddressOrHex(t *testing.T) {
	in := "NPAsqZkx9WhNd4P72uhZxBhLinSuNkxfB8"
	inHex, _ := address.StringToUint160(in)

	t.Run("Address", func(t *testing.T) {
		p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, in))}
		u, err := p.GetUint160FromAddressOrHex()
		require.NoError(t, err)
		require.Equal(t, inHex, u)
	})

	t.Run("Hex", func(t *testing.T) {
		p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, inHex.StringLE()))}
		u, err := p.GetUint160FromAddressOrHex()
		require.NoError(t, err)
		require.Equal(t, inHex, u)
	})
}

func TestParamGetFuncParam(t *testing.T) {
	fp := FuncParam{
		Type:  smartcontract.StringType,
		Value: Param{RawMessage: []byte(`"jajaja"`)},
	}
	p := Param{RawMessage: []byte(`{"type": "String", "value": "jajaja"}`)}
	newfp, err := p.GetFuncParam()
	assert.Equal(t, fp, newfp)
	require.Nil(t, err)

	p = Param{RawMessage: []byte(`42`)}
	_, err = p.GetFuncParam()
	require.NotNil(t, err)
}

func TestParamGetFuncParamPair(t *testing.T) {
	fp := FuncParam{
		Type:  smartcontract.MapType,
		Value: Param{RawMessage: []byte(`[{"key": {"type": "String", "value": "key1"}, "value": {"type": "Integer", "value": 123}}, {"key": {"type": "String", "value": "key2"}, "value": {"type": "Integer", "value": 456}}]`)},
	}
	p := Param{RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "String", "value": "key1"}, "value": {"type": "Integer", "value": 123}}, {"key": {"type": "String", "value": "key2"}, "value": {"type": "Integer", "value": 456}}]}`)}
	newfp, err := p.GetFuncParam()
	assert.Equal(t, fp, newfp)
	require.NoError(t, err)

	kvs, err := newfp.Value.GetArray()
	require.NoError(t, err)

	p1, err := kvs[0].GetFuncParamPair()
	require.NoError(t, err)

	fp1Key := FuncParam{
		Type:  smartcontract.StringType,
		Value: Param{RawMessage: []byte(`"key1"`)},
	}
	fp1Value := FuncParam{
		Type:  smartcontract.IntegerType,
		Value: Param{RawMessage: []byte(`123`)},
	}
	assert.Equal(t, fp1Key, p1.Key)
	assert.Equal(t, fp1Value, p1.Value)

	p2, err := kvs[1].GetFuncParamPair()
	require.NoError(t, err)

	fp2Key := FuncParam{
		Type:  smartcontract.StringType,
		Value: Param{RawMessage: []byte(`"key2"`)},
	}
	fp2Value := FuncParam{
		Type:  smartcontract.IntegerType,
		Value: Param{RawMessage: []byte(`456`)},
	}
	assert.Equal(t, fp2Key, p2.Key)
	assert.Equal(t, fp2Value, p2.Value)
}

func TestParamGetBytesHex(t *testing.T) {
	in := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"
	inb, _ := hex.DecodeString(in)
	p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, in))}
	bh, err := p.GetBytesHex()
	assert.Equal(t, inb, bh)
	require.Nil(t, err)

	p = Param{RawMessage: []byte(`42`)}
	h, err := p.GetBytesHex()
	assert.Equal(t, []byte{0x42}, h) // that's the way how C# works: 42 -> "42" -> []byte{0x42}
	require.Nil(t, err)

	p = Param{RawMessage: []byte(`"qq2c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"`)}
	_, err = p.GetBytesHex()
	require.NotNil(t, err)
}

func TestParamGetBytesBase64(t *testing.T) {
	in := "Aj4A8DoW6HB84EXrQu6A05JFFUHuUQ3BjhyL77rFTXQm"
	inb, err := base64.StdEncoding.DecodeString(in)
	require.NoError(t, err)
	p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, in))}
	bh, err := p.GetBytesBase64()
	assert.Equal(t, inb, bh)
	require.Nil(t, err)

	p = Param{RawMessage: []byte(`42`)}
	_, err = p.GetBytesBase64()
	require.NotNil(t, err)

	p = Param{RawMessage: []byte("@j4A8DoW6HB84EXrQu6A05JFFUHuUQ3BjhyL77rFTXQm")}
	_, err = p.GetBytesBase64()
	require.NotNil(t, err)
}

func TestParamGetSigner(t *testing.T) {
	c := tutusrpc.SignerWithWitness{
		Signer: transaction.Signer{
			Account: util.Uint160{1, 2, 3, 4},
			Scopes:  transaction.Global,
		},
		Witness: transaction.Witness{

			InvocationScript:   []byte{1, 2, 3},
			VerificationScript: []byte{1, 2, 3},
		},
	}
	p := Param{RawMessage: []byte(`{"account": "0x0000000000000000000000000000000004030201", "scopes": "Global", "invocation": "AQID", "verification": "AQID"}`)}
	actual, err := p.GetSignerWithWitness()
	require.NoError(t, err)
	require.Equal(t, c, actual)

	p = Param{RawMessage: []byte(`"not a signer"`)}
	_, err = p.GetSignerWithWitness()
	require.Error(t, err)
}

func TestParamGetSigners(t *testing.T) {
	u1 := util.Uint160{1, 2, 3, 4}
	u2 := util.Uint160{5, 6, 7, 8}
	t.Run("from hashes", func(t *testing.T) {
		p := Param{RawMessage: []byte(fmt.Sprintf(`["%s", "%s"]`, u1.StringLE(), u2.StringLE()))}
		actual, _, err := p.GetSignersWithWitnesses()
		require.NoError(t, err)
		require.Equal(t, 2, len(actual))
		require.True(t, u1.Equals(actual[0].Account))
		require.True(t, u2.Equals(actual[1].Account))
	})

	t.Run("overflow", func(t *testing.T) {
		var hashes = make([]util.Uint256, transaction.MaxAttributes+1)
		msg, err := json.Marshal(hashes)
		require.NoError(t, err)
		p := Param{RawMessage: msg}
		_, _, err = p.GetSignersWithWitnesses()
		require.Error(t, err)
	})

	t.Run("bad format", func(t *testing.T) {
		p := Param{RawMessage: []byte(`"not a signer"`)}
		_, _, err := p.GetSignersWithWitnesses()
		require.Error(t, err)
	})
}

func TestParamGetUUID(t *testing.T) {
	t.Run("from null", func(t *testing.T) {
		p := Param{RawMessage: []byte("null")}
		_, err := p.GetUUID()
		require.ErrorIs(t, err, errNotAString)
	})
	t.Run("invalid uuid", func(t *testing.T) {
		p := Param{RawMessage: []byte(`"not-a-uuid"`)}
		_, err := p.GetUUID()
		require.Error(t, err)
		require.True(t, strings.Contains(err.Error(), "not a valid UUID"), err.Error())
	})
	t.Run("compat", func(t *testing.T) {
		expected := "2107da59-4f9c-462c-9c51-7666842519a9"
		p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, expected))}
		id, err := p.GetUUID()
		require.NoError(t, err)
		require.Equal(t, id.String(), expected)
	})
}
