package native_test

import (
	"testing"

	"github.com/stretchr/testify/require"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

func newRoleRegistryClient(t *testing.T) *tutustest.ContractInvoker {
	return newNativeClient(t, nativenames.RoleRegistry)
}

// TestRoleRegistry_BuiltinRoles tests that built-in roles are created at initialization.
func TestRoleRegistry_BuiltinRoles(t *testing.T) {
	c := newRoleRegistryClient(t)

	// Check totalRoles returns at least 4 (the built-in roles)
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		count, err := stack[0].TryInteger()
		require.NoError(t, err)
		require.GreaterOrEqual(t, count.Int64(), int64(4))
	}, "totalRoles")

	// Check COMMITTEE role exists (ID=1)
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		// Should be an array (role struct)
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array for role")
		require.Equal(t, 7, len(arr)) // Role has 7 fields

		// Check role ID is 1
		id, err := arr[0].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(1), id.Int64())

		// Check name is COMMITTEE
		name, err := arr[1].TryBytes()
		require.NoError(t, err)
		require.Equal(t, "COMMITTEE", string(name))

		// Check role is active
		active, err := arr[6].TryBool()
		require.NoError(t, err)
		require.True(t, active)
	}, "getRole", 1)

	// Check REGISTRAR role exists (ID=2)
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array for role")
		name, err := arr[1].TryBytes()
		require.NoError(t, err)
		require.Equal(t, "REGISTRAR", string(name))
	}, "getRole", 2)

	// Check ATTESTOR role exists (ID=3)
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array for role")
		name, err := arr[1].TryBytes()
		require.NoError(t, err)
		require.Equal(t, "ATTESTOR", string(name))
	}, "getRole", 3)

	// Check OPERATOR role exists (ID=4)
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array for role")
		name, err := arr[1].TryBytes()
		require.NoError(t, err)
		require.Equal(t, "OPERATOR", string(name))
	}, "getRole", 4)
}

// TestRoleRegistry_GetRoleByName tests looking up roles by name.
func TestRoleRegistry_GetRoleByName(t *testing.T) {
	c := newRoleRegistryClient(t)

	// Look up COMMITTEE by name
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array for role")
		id, err := arr[0].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(1), id.Int64())
	}, "getRoleByName", "COMMITTEE")

	// Look up non-existent role by name
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value())
	}, "getRoleByName", "NONEXISTENT")
}

// TestRoleRegistry_HasRole tests checking if an address has a role.
func TestRoleRegistry_HasRole(t *testing.T) {
	c := newRoleRegistryClient(t)
	e := c.Executor

	acc := e.NewAccount(t)

	// Account should not have COMMITTEE role initially
	c.Invoke(t, false, "hasRole", acc.ScriptHash(), 1)

	// Grant COMMITTEE role (committee only)
	committeeInvoker := c.WithSigners(c.Committee)
	committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), 1, 0)

	// Now account should have COMMITTEE role
	c.Invoke(t, true, "hasRole", acc.ScriptHash(), 1)
}

// TestRoleRegistry_GrantRevokeRole tests granting and revoking roles.
func TestRoleRegistry_GrantRevokeRole(t *testing.T) {
	c := newRoleRegistryClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	committeeInvoker := c.WithSigners(c.Committee)

	// Non-committee cannot grant roles
	userInvoker := c.WithSigners(acc)
	userInvoker.InvokeFail(t, "caller is not a committee member", "grantRole", acc.ScriptHash(), 2, 0)

	// Committee can grant role
	committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), 2, 0)

	// Check role is granted
	c.Invoke(t, true, "hasRole", acc.ScriptHash(), 2)

	// Get roles for address
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array")
		require.GreaterOrEqual(t, len(arr), 1)
	}, "getRolesForAddress", acc.ScriptHash())

	// Non-committee cannot revoke roles
	userInvoker.InvokeFail(t, "caller is not a committee member", "revokeRole", acc.ScriptHash(), 2)

	// Committee can revoke role
	committeeInvoker.Invoke(t, true, "revokeRole", acc.ScriptHash(), 2)

	// Check role is revoked
	c.Invoke(t, false, "hasRole", acc.ScriptHash(), 2)
}

// TestRoleRegistry_CreateRole tests creating custom roles.
func TestRoleRegistry_CreateRole(t *testing.T) {
	c := newRoleRegistryClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	committeeInvoker := c.WithSigners(c.Committee)

	// Non-committee cannot create roles
	userInvoker := c.WithSigners(acc)
	userInvoker.InvokeFail(t, "caller is not a committee member", "createRole", "CUSTOM_ROLE", "A custom role", 0)

	// Committee can create role
	committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		roleID, err := stack[0].TryInteger()
		require.NoError(t, err)
		require.GreaterOrEqual(t, roleID.Int64(), int64(5)) // Custom roles start at 5
	}, "createRole", "CUSTOM_ROLE", "A custom role", 0)

	// Verify role exists
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array for role")
		name, err := arr[1].TryBytes()
		require.NoError(t, err)
		require.Equal(t, "CUSTOM_ROLE", string(name))
	}, "getRoleByName", "CUSTOM_ROLE")

	// Cannot create duplicate role name
	committeeInvoker.InvokeFail(t, "role name already exists", "createRole", "CUSTOM_ROLE", "Duplicate", 0)
}

// TestRoleRegistry_DeleteRole tests deleting (deactivating) roles.
func TestRoleRegistry_DeleteRole(t *testing.T) {
	c := newRoleRegistryClient(t)
	committeeInvoker := c.WithSigners(c.Committee)

	// Create a custom role first
	var customRoleID int64
	committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		roleID, _ := stack[0].TryInteger()
		customRoleID = roleID.Int64()
	}, "createRole", "DELETE_TEST", "Role to delete", 0)

	// Cannot delete built-in roles
	committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 1)
	committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 2)
	committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 3)
	committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 4)

	// Can delete custom role
	committeeInvoker.Invoke(t, true, "deleteRole", customRoleID)

	// Verify role is deactivated (still exists but not active)
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array for role")
		active, err := arr[6].TryBool()
		require.NoError(t, err)
		require.False(t, active)
	}, "getRole", customRoleID)
}

// TestRoleRegistry_Permissions tests assigning and checking permissions.
func TestRoleRegistry_Permissions(t *testing.T) {
	c := newRoleRegistryClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	committeeInvoker := c.WithSigners(c.Committee)

	// Create a custom role
	var customRoleID int64
	committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		roleID, _ := stack[0].TryInteger()
		customRoleID = roleID.Int64()
	}, "createRole", "PERM_TEST", "Permission test role", 0)

	// Assign permission to role
	committeeInvoker.Invoke(t, true, "assignPermission", customRoleID, "documents", "read", 0)

	// Grant role to account
	committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), customRoleID, 0)

	// Check account has permission
	c.Invoke(t, true, "hasPermission", acc.ScriptHash(), "documents", "read", 0)

	// Check account does NOT have other permission
	c.Invoke(t, false, "hasPermission", acc.ScriptHash(), "documents", "write", 0)

	// Get permissions for role
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array")
		require.Equal(t, 1, len(arr))
	}, "getPermissions", customRoleID)

	// Remove permission
	committeeInvoker.Invoke(t, true, "removePermission", customRoleID, "documents", "read")

	// Check permission is gone
	c.Invoke(t, false, "hasPermission", acc.ScriptHash(), "documents", "read", 0)
}

// TestRoleRegistry_RoleHierarchy tests role hierarchy (parent roles).
func TestRoleRegistry_RoleHierarchy(t *testing.T) {
	c := newRoleRegistryClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	committeeInvoker := c.WithSigners(c.Committee)

	// Create parent role
	var parentRoleID int64
	committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		roleID, _ := stack[0].TryInteger()
		parentRoleID = roleID.Int64()
	}, "createRole", "PARENT_ROLE", "Parent role", 0)

	// Create child role with parent
	var childRoleID int64
	committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		roleID, _ := stack[0].TryInteger()
		childRoleID = roleID.Int64()
	}, "createRole", "CHILD_ROLE", "Child role", parentRoleID)

	// Grant child role to account
	committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), childRoleID, 0)

	// Check account has child role
	c.Invoke(t, true, "hasRole", acc.ScriptHash(), childRoleID)

	// Check account ALSO has parent role through hierarchy
	c.Invoke(t, true, "hasRole", acc.ScriptHash(), parentRoleID)
}
