package nef

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

	"github.com/nspcc-dev/neo-go/internal/random"
	"github.com/nspcc-dev/neo-go/internal/testserdes"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

func TestEncodeDecodeBinary(t *testing.T) {
	script := []byte{12, 32, 84, 35, 14}
	expected := &File{
		Header: Header{
			Magic:    Magic,
			Compiler: "best compiler version 1",
		},
		Tokens: []MethodToken{{
			Hash:       random.Uint160(),
			Method:     "method",
			ParamCount: 3,
			HasReturn:  true,
			CallFlag:   callflag.WriteStates,
		}},
		Script: script,
	}

	t.Run("invalid Magic", func(t *testing.T) {
		expected.Header.Magic = 123
		checkDecodeError(t, expected)
	})

	t.Run("invalid checksum", func(t *testing.T) {
		expected.Header.Magic = Magic
		expected.Checksum = 123
		checkDecodeError(t, expected)
	})

	t.Run("zero-length script", func(t *testing.T) {
		expected.Script = make([]byte, 0)
		expected.Checksum = expected.CalculateChecksum()
		checkDecodeError(t, expected)
	})

	t.Run("invalid script length", func(t *testing.T) {
		newScript := make([]byte, stackitem.MaxSize+1)
		expected.Script = newScript
		expected.Checksum = expected.CalculateChecksum()
		checkDecodeError(t, expected)
	})
	t.Run("invalid tokens list", func(t *testing.T) {
		expected.Script = script
		expected.Tokens[0].Method = "_reserved"
		expected.Checksum = expected.CalculateChecksum()
		checkDecodeError(t, expected)
	})

	t.Run("positive", func(t *testing.T) {
		expected.Script = script
		expected.Tokens[0].Method = "method"
		expected.Checksum = expected.CalculateChecksum()
		expected.Header.Magic = Magic
		testserdes.EncodeDecodeBinary(t, expected, &File{})
	})
	t.Run("positive with empty tokens", func(t *testing.T) {
		expected.Script = script
		// Check `Tokens` won't be deserialized to `nil`. We expect them to be non-nil slice in the related code.
		expected.Tokens = []MethodToken{}
		expected.Checksum = expected.CalculateChecksum()
		expected.Header.Magic = Magic
		testserdes.EncodeDecodeBinary(t, expected, &File{})
	})
	t.Run("invalid reserved bytes", func(t *testing.T) {
		expected.Script = script
		expected.Tokens = expected.Tokens[:0]
		expected.Checksum = expected.CalculateChecksum()
		bytes, err := testserdes.EncodeBinary(expected)
		require.NoError(t, err)

		sz := io.GetVarSize(&expected.Header)
		bytes[sz] = 1
		err = testserdes.DecodeBinary(bytes, new(File))
		require.ErrorIs(t, err, errInvalidReserved)

		bytes[sz] = 0
		bytes[sz+3] = 1
		err = testserdes.DecodeBinary(bytes, new(File))
		require.ErrorIs(t, err, errInvalidReserved)
	})
}

func checkDecodeError(t *testing.T, expected *File) {
	bytes, err := testserdes.EncodeBinary(expected)
	require.NoError(t, err)
	require.Error(t, testserdes.DecodeBinary(bytes, &File{}))
}

func TestBytesFromBytes(t *testing.T) {
	script := []byte{12, 32, 84, 35, 14}
	expected := File{
		Header: Header{
			Magic:    Magic,
			Compiler: "best compiler version 1",
		},
		Tokens: []MethodToken{{
			Hash:       random.Uint160(),
			Method:     "someMethod",
			ParamCount: 3,
			HasReturn:  true,
			CallFlag:   callflag.WriteStates,
		}},
		Script: script,
	}
	expected.Checksum = expected.CalculateChecksum()

	bytes, err := expected.Bytes()
	require.NoError(t, err)
	actual, err := FileFromBytes(bytes)
	require.NoError(t, err)
	require.Equal(t, expected, actual)
}

func TestNewFileFromBytesLimits(t *testing.T) {
	expected := File{
		Header: Header{
			Magic:    Magic,
			Compiler: "best compiler version 1",
		},
		Tokens: []MethodToken{{
			Hash:       random.Uint160(),
			Method:     "someMethod",
			ParamCount: 3,
			HasReturn:  true,
			CallFlag:   callflag.WriteStates,
		}},
		Script: make([]byte, stackitem.MaxSize-100),
	}
	expected.Checksum = expected.CalculateChecksum()

	bytes, err := expected.BytesLong()
	require.NoError(t, err)
	_, err = FileFromBytes(bytes)
	require.Error(t, err)
	require.Contains(t, err.Error(), "invalid NEF file size")
}

func TestMarshalUnmarshalJSON(t *testing.T) {
	expected := &File{
		Header: Header{
			Magic:    Magic,
			Compiler: "test.compiler-test.ver",
		},
		Tokens: []MethodToken{{
			Hash:       util.Uint160{0x12, 0x34, 0x56, 0x78, 0x91, 0x00},
			Method:     "someMethod",
			ParamCount: 3,
			HasReturn:  true,
			CallFlag:   callflag.WriteStates,
		}},
		Script: []byte{1, 2, 3, 4},
	}
	expected.Checksum = expected.CalculateChecksum()

	data, err := json.Marshal(expected)
	require.NoError(t, err)
	require.JSONEq(t, `{
		"magic":`+strconv.FormatUint(uint64(Magic), 10)+`,
		"compiler": "test.compiler-test.ver",
		"source": "",
		"tokens": [
			{
	"hash": "0x`+expected.Tokens[0].Hash.StringLE()+`",
	"method": "someMethod",
	"paramcount": 3,
	"hasreturnvalue": true,
	"callflags": "`+expected.Tokens[0].CallFlag.String()+`"
			}
		],
		"script": "`+base64.StdEncoding.EncodeToString(expected.Script)+`",
		"checksum":`+strconv.FormatUint(uint64(expected.Checksum), 10)+`}`, string(data))

	actual := new(File)
	require.NoError(t, json.Unmarshal(data, actual))
	require.Equal(t, expected, actual)
}