package manifest

import (
	"encoding/json"
	"fmt"
	"reflect"
	"testing"

	"github.com/nspcc-dev/neo-go/internal/random"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

func TestNewPermission(t *testing.T) {
	require.Panics(t, func() { NewPermission(PermissionWildcard, util.Uint160{}) })
	require.Panics(t, func() { NewPermission(PermissionHash) })
	require.Panics(t, func() { NewPermission(PermissionHash, 1) })
	require.Panics(t, func() { NewPermission(PermissionGroup) })
	require.Panics(t, func() { NewPermission(PermissionGroup, util.Uint160{}) })
}

func TestPermissionIsValid(t *testing.T) {
	p := Permission{}
	require.NoError(t, p.IsValid())

	p.Methods.Add("")
	require.Error(t, p.IsValid())

	p.Methods.Value = nil
	p.Methods.Add("qwerty")
	require.NoError(t, p.IsValid())

	p.Methods.Add("poiuyt")
	require.NoError(t, p.IsValid())

	p.Methods.Add("qwerty")
	require.Error(t, p.IsValid())
}

func TestPermissionsAreValid(t *testing.T) {
	p := Permissions{}
	require.NoError(t, p.AreValid())

	p = append(p, Permission{Methods: WildStrings{Value: []string{""}}})
	require.Error(t, p.AreValid())

	p = p[:0]
	p = append(p, *NewPermission(PermissionHash, util.Uint160{1, 2, 3}))
	require.NoError(t, p.AreValid())

	priv0, err := keys.NewPrivateKey()
	require.NoError(t, err)
	priv1, err := keys.NewPrivateKey()
	require.NoError(t, err)

	p = append(p, *NewPermission(PermissionGroup, priv0.PublicKey()))
	require.NoError(t, p.AreValid())

	p = append(p, *NewPermission(PermissionGroup, priv1.PublicKey()))
	require.NoError(t, p.AreValid())

	p = append(p, *NewPermission(PermissionWildcard))
	require.NoError(t, p.AreValid())

	p = append(p, *NewPermission(PermissionHash, util.Uint160{3, 2, 1}))
	require.NoError(t, p.AreValid())

	p = append(p, *NewPermission(PermissionWildcard))
	require.Error(t, p.AreValid())

	p = append(p[:len(p)-1], *NewPermission(PermissionHash, util.Uint160{1, 2, 3}))
	require.Error(t, p.AreValid())

	p = append(p[:len(p)-1], *NewPermission(PermissionGroup, priv0.PublicKey()))
	require.Error(t, p.AreValid())
}

func TestPermission_MarshalJSON(t *testing.T) {
	t.Run("wildcard", func(t *testing.T) {
		expected := NewPermission(PermissionWildcard)
		expected.Methods.Restrict()
		testMarshalUnmarshal(t, expected, NewPermission(PermissionWildcard))
	})

	t.Run("group", func(t *testing.T) {
		expected := NewPermission(PermissionWildcard)
		expected.Contract.Type = PermissionGroup
		priv, err := keys.NewPrivateKey()
		require.NoError(t, err)
		expected.Contract.Value = priv.PublicKey()
		expected.Methods.Add("method1")
		expected.Methods.Add("method2")
		testMarshalUnmarshal(t, expected, NewPermission(PermissionWildcard))
	})

	t.Run("hash", func(t *testing.T) {
		expected := NewPermission(PermissionWildcard)
		expected.Contract.Type = PermissionHash
		expected.Contract.Value = random.Uint160()
		testMarshalUnmarshal(t, expected, NewPermission(PermissionWildcard))
	})
}

func TestPermissionDesc_MarshalJSON(t *testing.T) {
	t.Run("uint160 with 0x", func(t *testing.T) {
		u := random.Uint160()
		s := u.StringLE()
		js := []byte(fmt.Sprintf(`"0x%s"`, s))
		d := new(PermissionDesc)
		require.NoError(t, json.Unmarshal(js, d))
		require.Equal(t, u, d.Value.(util.Uint160))
	})

	t.Run("invalid uint160", func(t *testing.T) {
		d := new(PermissionDesc)
		s := random.String(util.Uint160Size * 2)
		js := []byte(fmt.Sprintf(`"ok%s"`, s))
		require.Error(t, json.Unmarshal(js, d))

		js = []byte(fmt.Sprintf(`"%s"`, s))
		require.Error(t, json.Unmarshal(js, d))
	})

	t.Run("invalid public key", func(t *testing.T) {
		d := new(PermissionDesc)
		s := random.String(65)
		s = "k" + s // not a hex
		js := []byte(fmt.Sprintf(`"%s"`, s))
		require.Error(t, json.Unmarshal(js, d))
	})

	t.Run("not a string", func(t *testing.T) {
		d := new(PermissionDesc)
		js := []byte(`123`)
		require.Error(t, json.Unmarshal(js, d))
	})

	t.Run("invalid string", func(t *testing.T) {
		d := new(PermissionDesc)
		js := []byte(`"invalid length"`)
		require.Error(t, json.Unmarshal(js, d))
	})
}

func testMarshalUnmarshal(t *testing.T, expected, actual any) {
	data, err := json.Marshal(expected)
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(data, actual))
	require.Equal(t, expected, actual)
}

func TestPermission_ToStackItemFromStackItem(t *testing.T) {
	t.Run("wildcard", func(t *testing.T) {
		p := NewPermission(PermissionWildcard)
		expected := stackitem.NewStruct([]stackitem.Item{
			stackitem.Null{},
			stackitem.Null{},
		})
		CheckToFromStackItem(t, p, expected)
	})

	t.Run("hash", func(t *testing.T) {
		p := NewPermission(PermissionHash, util.Uint160{1, 2, 3})
		p.Methods = WildStrings{Value: []string{"a"}}
		expected := stackitem.NewStruct([]stackitem.Item{
			stackitem.NewByteArray(util.Uint160{1, 2, 3}.BytesBE()),
			stackitem.NewArray([]stackitem.Item{
				stackitem.NewByteArray([]byte("a")),
			}),
		})
		CheckToFromStackItem(t, p, expected)
	})

	t.Run("group", func(t *testing.T) {
		pk, _ := keys.NewPrivateKey()
		p := NewPermission(PermissionGroup, pk.PublicKey())
		expected := stackitem.NewStruct([]stackitem.Item{
			stackitem.NewByteArray(pk.PublicKey().Bytes()),
			stackitem.Null{},
		})
		CheckToFromStackItem(t, p, expected)
	})
}

type Interoperable interface {
	ToStackItem() stackitem.Item
	FromStackItem(stackitem.Item) error
}

func CheckToFromStackItem(t *testing.T, source Interoperable, expected stackitem.Item) {
	actual := source.ToStackItem()
	require.Equal(t, expected, actual)
	actualSource := reflect.New(reflect.TypeOf(source).Elem()).Interface().(Interoperable)
	require.NoError(t, actualSource.FromStackItem(actual))
	require.Equal(t, source, actualSource)
}

func TestPermission_FromStackItemErrors(t *testing.T) {
	errCases := map[string]stackitem.Item{
		"not a struct":            stackitem.NewArray([]stackitem.Item{}),
		"invalid length":          stackitem.NewStruct([]stackitem.Item{}),
		"invalid contract type":   stackitem.NewStruct([]stackitem.Item{stackitem.NewArray([]stackitem.Item{}), stackitem.NewBool(false)}),
		"invalid contract length": stackitem.NewStruct([]stackitem.Item{stackitem.NewByteArray([]byte{1, 2, 3}), stackitem.NewBool(false)}),
		"invalid contract pubkey": stackitem.NewStruct([]stackitem.Item{stackitem.NewByteArray(make([]byte, 33)), stackitem.NewBool(false)}),
		"invalid methods type":    stackitem.NewStruct([]stackitem.Item{stackitem.Null{}, stackitem.NewBool(false)}),
		"invalid method name":     stackitem.NewStruct([]stackitem.Item{stackitem.Null{}, stackitem.NewArray([]stackitem.Item{stackitem.NewArray([]stackitem.Item{})})}),
	}
	for name, errCase := range errCases {
		t.Run(name, func(t *testing.T) {
			p := new(Permission)
			require.Error(t, p.FromStackItem(errCase))
		})
	}
}