package native_test

import (
	"testing"
	"time"

	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
	"github.com/stretchr/testify/require"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

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

// registerVita is a helper to register a Vita for a signer.
// Returns the tokenID bytes.
func registerVita(t *testing.T, c *tutustest.ContractInvoker, signer tutustest.Signer) []byte {
	owner := signer.ScriptHash()
	personHash := hash.Sha256(owner.BytesBE()).BytesBE()
	isEntity := false
	recoveryHash := hash.Sha256([]byte("recovery")).BytesBE()
	birthTimestamp := time.Now().Unix() - 30*365*24*60*60 // Default: 30 years old

	invoker := c.WithSigners(signer)
	// Register returns tokenID bytes, not null
	txHash := invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		// Result is a ByteArray (tokenID)
		_, ok := stack[0].Value().([]byte)
		require.True(t, ok, "expected ByteArray result")
	}, "register", owner.BytesBE(), personHash, isEntity, recoveryHash, birthTimestamp)

	aer := c.Executor.GetTxExecResult(t, txHash)
	require.Equal(t, 1, len(aer.Stack))
	tokenIDBytes := aer.Stack[0].Value().([]byte)
	return tokenIDBytes
}

// TestVita_Register tests basic registration functionality.
func TestVita_Register(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	owner := acc.ScriptHash()
	personHash := hash.Sha256(owner.BytesBE()).BytesBE()
	isEntity := false
	recoveryHash := hash.Sha256([]byte("recovery")).BytesBE()
	birthTimestamp := time.Now().Unix() - 30*365*24*60*60 // 30 years old

	invoker := c.WithSigners(acc)

	// Register token - returns tokenID bytes
	txHash := invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		_, ok := stack[0].Value().([]byte)
		require.True(t, ok, "expected ByteArray result")
	}, "register", owner.BytesBE(), personHash, isEntity, recoveryHash, birthTimestamp)

	// Check events were emitted (BirthRegistered from Annos + VitaCreated)
	aer := e.GetTxExecResult(t, txHash)
	require.Equal(t, 2, len(aer.Events))
	require.Equal(t, "BirthRegistered", aer.Events[0].Name)
	require.Equal(t, "VitaCreated", aer.Events[1].Name)

	// Check exists returns true
	invoker.Invoke(t, true, "exists", owner.BytesBE())

	// Check getToken returns valid token
	invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr := stack[0].Value().([]stackitem.Item)
		require.GreaterOrEqual(t, len(arr), 5) // At least tokenID, owner, personHash, isEntity, status
	}, "getToken", owner.BytesBE())
}

// TestVita_ValidateCaller tests the validateCaller method.
// Note: validateCaller uses GetCallingScriptHash() which returns the calling contract's
// script hash, not the transaction signer's address. When called directly from a transaction
// (not from another contract), the caller has no token. These methods are designed for
// cross-contract authorization.
func TestVita_ValidateCaller(t *testing.T) {
	c := newVitaClient(t)

	t.Run("no token - direct call", func(t *testing.T) {
		acc := c.Executor.NewAccount(t)
		invoker := c.WithSigners(acc)

		// validateCaller uses GetCallingScriptHash() which returns the transaction script hash
		// when called directly, not the signer's account. This will always fail for direct calls.
		invoker.InvokeFail(t, "caller does not have a Vita", "validateCaller")
	})

	// Note: Testing validateCaller with a token requires deploying a helper contract
	// that has a Vita registered to its script hash, then calling validateCaller
	// from within that contract. This is the intended usage pattern for cross-contract auth.
}

// TestVita_RequireRole tests the requireRole method.
// Note: requireRole uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestVita_RequireRole(t *testing.T) {
	c := newVitaClient(t)

	t.Run("no token - direct call", func(t *testing.T) {
		acc := c.Executor.NewAccount(t)
		invoker := c.WithSigners(acc)

		// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
		invoker.InvokeFail(t, "caller does not have a Vita", "requireRole", 0)
	})

	// Note: Testing requireRole with actual role checks requires a deployed contract
	// with a Vita registered to its script hash.
}

// TestVita_RequireCoreRole tests the requireCoreRole method.
// Note: requireCoreRole uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestVita_RequireCoreRole(t *testing.T) {
	c := newVitaClient(t)

	// CoreRole constants
	const (
		CoreRoleNone     = 0
		CoreRoleRecovery = 5
	)

	t.Run("invalid role", func(t *testing.T) {
		acc := c.Executor.NewAccount(t)
		invoker := c.WithSigners(acc)

		// requireCoreRole with invalid role (> 5) should fail before checking token
		invoker.InvokeFail(t, "invalid core role", "requireCoreRole", 10)
	})

	t.Run("no token - direct call", func(t *testing.T) {
		acc := c.Executor.NewAccount(t)
		invoker := c.WithSigners(acc)

		// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
		invoker.InvokeFail(t, "caller does not have a Vita", "requireCoreRole", CoreRoleNone)
	})

	// Note: Testing requireCoreRole with actual role checks requires a deployed contract
	// with a Vita registered to its script hash.
}

// TestVita_RequirePermission tests the requirePermission method.
// Note: requirePermission uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestVita_RequirePermission(t *testing.T) {
	c := newVitaClient(t)

	t.Run("empty resource", func(t *testing.T) {
		acc := c.Executor.NewAccount(t)
		invoker := c.WithSigners(acc)

		// requirePermission with empty resource should fail before checking token
		invoker.InvokeFail(t, "invalid resource", "requirePermission", "", "read", "global")
	})

	t.Run("empty action", func(t *testing.T) {
		acc := c.Executor.NewAccount(t)
		invoker := c.WithSigners(acc)

		// requirePermission with empty action should fail before checking token
		invoker.InvokeFail(t, "invalid action", "requirePermission", "documents", "", "global")
	})

	t.Run("no token - direct call", func(t *testing.T) {
		acc := c.Executor.NewAccount(t)
		invoker := c.WithSigners(acc)

		// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
		invoker.InvokeFail(t, "caller does not have a Vita", "requirePermission", "documents", "read", "global")
	})

	// Note: Testing requirePermission with actual permission checks requires a deployed contract
	// with a Vita registered to its script hash.
}

// TestVita_TotalSupply tests the totalSupply method.
func TestVita_TotalSupply(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	// Initially, totalSupply should be 0
	c.Invoke(t, 0, "totalSupply")

	// Register a token
	acc1 := e.NewAccount(t)
	registerVita(t, c, acc1)

	// Now totalSupply should be 1
	c.Invoke(t, 1, "totalSupply")

	// Register another token
	acc2 := e.NewAccount(t)
	registerVita(t, c, acc2)

	// Now totalSupply should be 2
	c.Invoke(t, 2, "totalSupply")
}

// TestVita_GetTokenByID tests the getTokenByID method.
func TestVita_GetTokenByID(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	// Register a token - the first token gets ID 0 (counter starts at 0)
	acc := e.NewAccount(t)
	registerVita(t, c, acc)

	// Token ID 0 should exist (first registered token)
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		// Check that result is an array (not null)
		arr, ok := stack[0].Value().([]stackitem.Item)
		require.True(t, ok, "expected array result for existing token")
		require.GreaterOrEqual(t, len(arr), 9) // Vita has 9 fields

		// Check owner matches (owner is at index 1)
		owner, ok := arr[1].Value().([]byte)
		require.True(t, ok, "expected owner to be bytes")
		require.Equal(t, acc.ScriptHash().BytesBE(), owner)
	}, "getTokenByID", 0)

	// Non-existent token should return null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		// Null returns nil from Value()
		require.Nil(t, stack[0].Value(), "expected null for non-existent token")
	}, "getTokenByID", 999999)
}

// TestVita_SuspendReinstate tests suspend and reinstate functionality.
func TestVita_SuspendReinstate(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	registerVita(t, c, acc)

	invoker := c.WithSigners(acc)
	committeeInvoker := c.WithSigners(c.Committee)

	// Create Lex invoker using same executor for liberty restriction (required for due process)
	lexHash := e.NativeHash(t, nativenames.Lex)
	lexCommitteeInvoker := e.CommitteeInvoker(lexHash)

	// Initially token is active
	invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr := stack[0].Value().([]stackitem.Item)
		// Status is at index 6 (after tokenID, owner, personHash, isEntity, createdAt, updatedAt)
		status, err := arr[6].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(state.TokenStatusActive), status.Int64())
	}, "getToken", acc.ScriptHash().BytesBE())

	// Non-committee cannot suspend
	invoker.InvokeFail(t, "invalid committee signature", "suspend", acc.ScriptHash().BytesBE(), "test")

	// First, create a liberty restriction via Lex (due process requirement)
	caseID := hash.Sha256([]byte("case-123")).BytesBE() // 32-byte case reference
	lexCommitteeInvoker.Invoke(t, true, "restrictRight",
		acc.ScriptHash().BytesBE(),  // subject
		int64(state.RightLiberty),   // rightID = 2 (Liberty)
		int64(state.RestrictionSuspend), // restrictionType = 1 (Suspend)
		int64(100),                  // duration in blocks
		"judicial order",            // reason
		caseID,                      // caseID (Hash256)
	)

	// Committee can suspend (now with valid Lex restriction order)
	committeeInvoker.Invoke(t, true, "suspend", acc.ScriptHash().BytesBE(), "test suspension")

	// Token is now suspended
	invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr := stack[0].Value().([]stackitem.Item)
		status, err := arr[6].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(state.TokenStatusSuspended), status.Int64())
	}, "getToken", acc.ScriptHash().BytesBE())

	// Non-committee cannot reinstate
	invoker.InvokeFail(t, "invalid committee signature", "reinstate", acc.ScriptHash().BytesBE())

	// Committee can reinstate
	committeeInvoker.Invoke(t, true, "reinstate", acc.ScriptHash().BytesBE())

	// Token is active again
	invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr := stack[0].Value().([]stackitem.Item)
		status, err := arr[6].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(state.TokenStatusActive), status.Int64())
	}, "getToken", acc.ScriptHash().BytesBE())
}

// Note: Full cross-contract testing of validateCaller, requireRole, requireCoreRole, and
// requirePermission would require deploying a helper contract that:
// 1. Has a Vita registered to its script hash
// 2. Calls the Vita cross-contract methods from within its own methods
// This is the intended usage pattern for these cross-contract authorization methods.

// ============================================================================
// VPP (Vita Presence Protocol) Tests
// ============================================================================

// TestVita_GetPresenceConfig tests the getPresenceConfig method.
func TestVita_GetPresenceConfig(t *testing.T) {
	c := newVitaClient(t)

	// getPresenceConfig returns default config
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		arr := stack[0].Value().([]stackitem.Item)
		require.Equal(t, 4, len(arr), "expected 4 config values")

		// Verify default values
		presenceWindow, err := arr[0].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(60), presenceWindow.Int64(), "default presenceWindow should be 60")

		awayWindow, err := arr[1].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(300), awayWindow.Int64(), "default awayWindow should be 300")

		minHeartbeat, err := arr[2].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(15), minHeartbeat.Int64(), "default minHeartbeatInterval should be 15")

		maxHeartbeat, err := arr[3].TryInteger()
		require.NoError(t, err)
		require.Equal(t, int64(60), maxHeartbeat.Int64(), "default maxHeartbeatInterval should be 60")
	}, "getPresenceConfig")
}

// TestVita_GetPresence_NonExistent tests getPresence for non-existent token.
func TestVita_GetPresence_NonExistent(t *testing.T) {
	c := newVitaClient(t)

	// getPresence for non-existent tokenID should return null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Equal(t, stackitem.Null{}, stack[0], "expected null for non-existent presence")
	}, "getPresence", 99999)
}

// TestVita_GetPresenceStatus_NonExistent tests getPresenceStatus for non-existent token.
func TestVita_GetPresenceStatus_NonExistent(t *testing.T) {
	c := newVitaClient(t)

	// getPresenceStatus for non-existent tokenID should return 0 (offline)
	c.Invoke(t, int64(state.PresenceStatusOffline), "getPresenceStatus", 99999)
}

// TestVita_RecordPresence_NoVita tests recordPresence when caller has no Vita.
func TestVita_RecordPresence_NoVita(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	invoker := c.WithSigners(acc)

	sessionID := make([]byte, 32)
	deviceHash := make([]byte, 32)

	// recordPresence uses GetCallingScriptHash() - direct call fails because
	// the calling script hash (transaction) doesn't have a Vita
	invoker.InvokeFail(t, "caller does not have a Vita", "recordPresence", sessionID, deviceHash)
}

// TestVita_RecordPresence_InvalidSession tests recordPresence with invalid session.
func TestVita_RecordPresence_InvalidSession(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	invoker := c.WithSigners(acc)

	// Invalid session ID (not 32 bytes)
	sessionID := make([]byte, 16) // Wrong size
	deviceHash := make([]byte, 32)

	invoker.InvokeFail(t, "invalid session ID: must be 32 bytes", "recordPresence", sessionID, deviceHash)
}

// TestVita_RecordPresence_InvalidDevice tests recordPresence with invalid device hash.
func TestVita_RecordPresence_InvalidDevice(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	invoker := c.WithSigners(acc)

	sessionID := make([]byte, 32)
	deviceHash := make([]byte, 16) // Wrong size

	invoker.InvokeFail(t, "invalid device hash: must be 32 bytes", "recordPresence", sessionID, deviceHash)
}

// TestVita_SetPresenceVisibility_NoVita tests setPresenceVisibility when caller has no Vita.
func TestVita_SetPresenceVisibility_NoVita(t *testing.T) {
	c := newVitaClient(t)
	e := c.Executor

	acc := e.NewAccount(t)
	invoker := c.WithSigners(acc)

	// setPresenceVisibility uses GetCallingScriptHash() - direct call fails
	invoker.InvokeFail(t, "caller does not have a Vita", "setPresenceVisibility", true)
}

// Note: Full VPP testing of recordPresence and setPresenceVisibility with actual
// heartbeat recording would require deploying a helper contract that:
// 1. Has a Vita registered to its script hash
// 2. Calls recordPresence and setPresenceVisibility from within its own methods
// This enables testing the full VPP flow including:
// - Successful heartbeat recording
// - Rate limiting (MinHeartbeatInterval)
// - Presence status transitions (online -> away -> offline)
// - Invisible mode
