package request

import (
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"testing"

	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestParam_UnmarshalJSON(t *testing.T) {
	msg := `["str1", 123, null, ["str2", 3], [{"type": "String", "value": "jajaja"}],
                 {"primary": 1},
                 {"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
                 {"signer": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
                 {"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "signer": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
                 {"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
                 {"name": "my_pretty_notification"},
                 {"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "name":"my_pretty_notification"},
                 {"state": "HALT"},
                 {"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"},
                 {"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"},
                 [{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}]]`
	contr, err := util.Uint160DecodeStringLE("f84d6a337fbc3d3a201d41da99e86b479e7a2554")
	require.NoError(t, err)
	name := "my_pretty_notification"
	accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
	require.NoError(t, err)
	addrHash, err := address.StringToUint160("NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag")
	require.NoError(t, err)
	expected := Params{
		{
			Type:  StringT,
			Value: "str1",
		},
		{
			Type:  NumberT,
			Value: 123,
		},
		{
			Type: defaultT,
		},
		{
			Type: ArrayT,
			Value: []Param{
				{
					Type:  StringT,
					Value: "str2",
				},
				{
					Type:  NumberT,
					Value: 3,
				},
			},
		},
		{
			Type: ArrayT,
			Value: []Param{
				{
					Type: FuncParamT,
					Value: FuncParam{
						Type: smartcontract.StringType,
						Value: Param{
							Type:  StringT,
							Value: "jajaja",
						},
					},
				},
			},
		},
		{
			Type:  BlockFilterT,
			Value: BlockFilter{Primary: 1},
		},
		{
			Type:  TxFilterT,
			Value: TxFilter{Sender: &contr},
		},
		{
			Type:  TxFilterT,
			Value: TxFilter{Signer: &contr},
		},
		{
			Type:  TxFilterT,
			Value: TxFilter{Sender: &contr, Signer: &contr},
		},
		{
			Type:  NotificationFilterT,
			Value: NotificationFilter{Contract: &contr},
		},
		{
			Type:  NotificationFilterT,
			Value: NotificationFilter{Name: &name},
		},
		{
			Type:  NotificationFilterT,
			Value: NotificationFilter{Contract: &contr, Name: &name},
		},
		{
			Type:  ExecutionFilterT,
			Value: ExecutionFilter{State: "HALT"},
		},
		{
			Type: SignerWithWitnessT,
			Value: SignerWithWitness{
				Signer: transaction.Signer{
					Account: accountHash,
					Scopes:  transaction.None,
				},
			},
		},
		{
			Type: SignerWithWitnessT,
			Value: SignerWithWitness{
				Signer: transaction.Signer{
					Account: addrHash,
					Scopes:  transaction.Global,
				},
			},
		},
		{
			Type: ArrayT,
			Value: []Param{
				{
					Type: SignerWithWitnessT,
					Value: SignerWithWitness{
						Signer: transaction.Signer{
							Account: accountHash,
							Scopes:  transaction.Global,
						},
					},
				},
			},
		},
	}

	var ps Params
	require.NoError(t, json.Unmarshal([]byte(msg), &ps))
	require.Equal(t, expected, ps)

	msg = `[{"2": 3}]`
	require.Error(t, json.Unmarshal([]byte(msg), &ps))
	msg = `[{"account": "notanaccount", "scopes": "Global"}]`
	require.Error(t, json.Unmarshal([]byte(msg), &ps))
}

func TestParamGetString(t *testing.T) {
	p := Param{StringT, "jajaja"}
	str, err := p.GetString()
	assert.Equal(t, "jajaja", str)
	require.Nil(t, err)

	p = Param{StringT, int(100500)}
	_, err = p.GetString()
	require.NotNil(t, err)
}

func TestParamGetInt(t *testing.T) {
	p := Param{NumberT, int(100500)}
	i, err := p.GetInt()
	assert.Equal(t, 100500, i)
	require.Nil(t, err)

	p = Param{NumberT, "jajaja"}
	_, err = p.GetInt()
	require.NotNil(t, err)
}

func TestParamGetArray(t *testing.T) {
	p := Param{ArrayT, []Param{{NumberT, 42}}}
	a, err := p.GetArray()
	assert.Equal(t, []Param{{NumberT, 42}}, a)
	require.Nil(t, err)

	p = Param{ArrayT, 42}
	_, err = p.GetArray()
	require.NotNil(t, err)
}

func TestParamGetUint256(t *testing.T) {
	gas := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"
	u256, _ := util.Uint256DecodeStringLE(gas)
	p := Param{StringT, gas}
	u, err := p.GetUint256()
	assert.Equal(t, u256, u)
	require.Nil(t, err)

	p = Param{StringT, "0x" + gas}
	u, err = p.GetUint256()
	require.NoError(t, err)
	assert.Equal(t, u256, u)

	p = Param{StringT, 42}
	_, err = p.GetUint256()
	require.NotNil(t, err)

	p = Param{StringT, "qq2c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"}
	_, err = p.GetUint256()
	require.NotNil(t, err)
}

func TestParamGetUint160FromHex(t *testing.T) {
	in := "50befd26fdf6e4d957c11e078b24ebce6291456f"
	u160, _ := util.Uint160DecodeStringLE(in)
	p := Param{StringT, in}
	u, err := p.GetUint160FromHex()
	assert.Equal(t, u160, u)
	require.Nil(t, err)

	p = Param{StringT, 42}
	_, err = p.GetUint160FromHex()
	require.NotNil(t, err)

	p = Param{StringT, "wwbefd26fdf6e4d957c11e078b24ebce6291456f"}
	_, err = p.GetUint160FromHex()
	require.NotNil(t, err)
}

func TestParamGetUint160FromAddress(t *testing.T) {
	in := "NPAsqZkx9WhNd4P72uhZxBhLinSuNkxfB8"
	u160, _ := address.StringToUint160(in)
	p := Param{StringT, in}
	u, err := p.GetUint160FromAddress()
	assert.Equal(t, u160, u)
	require.Nil(t, err)

	p = Param{StringT, 42}
	_, err = p.GetUint160FromAddress()
	require.NotNil(t, err)

	p = Param{StringT, "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{StringT, in}
		u, err := p.GetUint160FromAddressOrHex()
		require.NoError(t, err)
		require.Equal(t, inHex, u)
	})

	t.Run("Hex", func(t *testing.T) {
		p := Param{StringT, 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{
			Type:  StringT,
			Value: "jajaja",
		},
	}
	p := Param{
		Type:  FuncParamT,
		Value: fp,
	}
	newfp, err := p.GetFuncParam()
	assert.Equal(t, fp, newfp)
	require.Nil(t, err)

	p = Param{FuncParamT, 42}
	_, err = p.GetFuncParam()
	require.NotNil(t, err)
}

func TestParamGetBytesHex(t *testing.T) {
	in := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"
	inb, _ := hex.DecodeString(in)
	p := Param{StringT, in}
	bh, err := p.GetBytesHex()
	assert.Equal(t, inb, bh)
	require.Nil(t, err)

	p = Param{StringT, 42}
	_, err = p.GetBytesHex()
	require.NotNil(t, err)

	p = Param{StringT, "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{StringT, in}
	bh, err := p.GetBytesBase64()
	assert.Equal(t, inb, bh)
	require.Nil(t, err)

	p = Param{StringT, 42}
	_, err = p.GetBytesBase64()
	require.NotNil(t, err)

	p = Param{StringT, "@j4A8DoW6HB84EXrQu6A05JFFUHuUQ3BjhyL77rFTXQm"}
	_, err = p.GetBytesBase64()
	require.NotNil(t, err)
}

func TestParamGetSigner(t *testing.T) {
	c := 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{Type: SignerWithWitnessT, Value: c}
	actual, err := p.GetSignerWithWitness()
	require.NoError(t, err)
	require.Equal(t, c, actual)

	p = Param{Type: SignerWithWitnessT, Value: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}`}
	_, 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{ArrayT, []Param{
			{Type: StringT, Value: u1.StringLE()},
			{Type: StringT, Value: 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("from signers", func(t *testing.T) {
		c1 := SignerWithWitness{
			Signer: transaction.Signer{
				Account: u1,
				Scopes:  transaction.Global,
			},
			Witness: transaction.Witness{
				InvocationScript:   []byte{1, 2, 3},
				VerificationScript: []byte{1, 2, 3},
			},
		}
		c2 := SignerWithWitness{
			Signer: transaction.Signer{
				Account: u2,
				Scopes:  transaction.CustomContracts,
				AllowedContracts: []util.Uint160{
					{1, 2, 3},
					{4, 5, 6},
				},
			},
		}
		p := Param{ArrayT, []Param{
			{Type: SignerWithWitnessT, Value: c1},
			{Type: SignerWithWitnessT, Value: c2},
		}}
		actualS, actualW, err := p.GetSignersWithWitnesses()
		require.NoError(t, err)
		require.Equal(t, 2, len(actualS))
		require.Equal(t, transaction.Signer{
			Account: c1.Account,
			Scopes:  c1.Scopes,
		}, actualS[0])
		require.Equal(t, transaction.Signer{
			Account:          c2.Account,
			Scopes:           c2.Scopes,
			AllowedContracts: c2.AllowedContracts,
		}, actualS[1])
		require.EqualValues(t, 2, len(actualW))
		require.EqualValues(t, transaction.Witness{
			InvocationScript:   c1.InvocationScript,
			VerificationScript: c1.VerificationScript,
		}, actualW[0])
		require.Equal(t, transaction.Witness{}, actualW[1])
	})

	t.Run("bad format", func(t *testing.T) {
		p := Param{ArrayT, []Param{
			{Type: StringT, Value: u1.StringLE()},
			{Type: StringT, Value: "bla"},
		}}
		_, _, err := p.GetSignersWithWitnesses()
		require.Error(t, err)
	})
}