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/tutustest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

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

// TestSese_GetConfig tests the getConfig method.
func TestSese_GetConfig(t *testing.T) {
	c := newSeseClient(t)

	// Get default config
	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 result")
		require.GreaterOrEqual(t, len(arr), 5) // SeseConfig has 5 fields
	}, "getConfig")
}

// TestSese_GetTotalAccounts tests the getTotalAccounts method.
func TestSese_GetTotalAccounts(t *testing.T) {
	c := newSeseClient(t)

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

// TestSese_GetTotalCareers tests the getTotalCareers method.
func TestSese_GetTotalCareers(t *testing.T) {
	c := newSeseClient(t)

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

// TestSese_GetTotalSabbaticals tests the getTotalSabbaticals method.
func TestSese_GetTotalSabbaticals(t *testing.T) {
	c := newSeseClient(t)

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

// TestSese_GetTotalMilestones tests the getTotalMilestones method.
func TestSese_GetTotalMilestones(t *testing.T) {
	c := newSeseClient(t)

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

// TestSese_GetTotalGoals tests the getTotalGoals method.
func TestSese_GetTotalGoals(t *testing.T) {
	c := newSeseClient(t)

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

// TestSese_GetAccount_NonExistent tests getting a non-existent account.
func TestSese_GetAccount_NonExistent(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	acc := e.NewAccount(t)

	// Non-existent account should return null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value(), "expected null for non-existent account")
	}, "getAccount", acc.ScriptHash())
}

// TestSese_GetBalance_NonExistent tests getting balance for non-existent account.
func TestSese_GetBalance_NonExistent(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	acc := e.NewAccount(t)

	// Non-existent account should return 0 balance
	c.Invoke(t, 0, "getBalance", acc.ScriptHash())
}

// TestSese_GetSabbaticalCredits_NonExistent tests getting sabbatical credits for non-existent account.
func TestSese_GetSabbaticalCredits_NonExistent(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	acc := e.NewAccount(t)

	// Non-existent account should return 0 credits
	c.Invoke(t, 0, "getSabbaticalCredits", acc.ScriptHash())
}

// TestSese_ActivateLifePlan_NoVita tests that activating life plan without Vita fails.
func TestSese_ActivateLifePlan_NoVita(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

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

	// Should fail - no Vita registered
	invoker.InvokeFail(t, "owner must have an active Vita", "activateLifePlan", acc.ScriptHash())
}

// TestSese_AllocateSabbaticalCredits_NotCommittee tests that non-committee cannot allocate credits.
func TestSese_AllocateSabbaticalCredits_NotCommittee(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

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

	// Should fail - not committee
	invoker.InvokeFail(t, "invalid committee signature", "allocateSabbaticalCredits",
		acc.ScriptHash(), 100, "test allocation")
}

// TestSese_GetCareer_NonExistent tests getting a non-existent career.
func TestSese_GetCareer_NonExistent(t *testing.T) {
	c := newSeseClient(t)

	// Non-existent career should return null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value(), "expected null for non-existent career")
	}, "getCareer", int64(999))
}

// TestSese_GetSabbatical_NonExistent tests getting a non-existent sabbatical.
func TestSese_GetSabbatical_NonExistent(t *testing.T) {
	c := newSeseClient(t)

	// Non-existent sabbatical should return null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value(), "expected null for non-existent sabbatical")
	}, "getSabbatical", int64(999))
}

// TestSese_GetMilestone_NonExistent tests getting a non-existent milestone.
func TestSese_GetMilestone_NonExistent(t *testing.T) {
	c := newSeseClient(t)

	// Non-existent milestone should return null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value(), "expected null for non-existent milestone")
	}, "getMilestone", int64(999))
}

// TestSese_GetGoal_NonExistent tests getting a non-existent goal.
func TestSese_GetGoal_NonExistent(t *testing.T) {
	c := newSeseClient(t)

	// Non-existent goal should return null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value(), "expected null for non-existent goal")
	}, "getGoal", int64(999))
}

// TestSese_GetActiveCareer_NoAccount tests getting active career for non-existent account.
func TestSese_GetActiveCareer_NoAccount(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	acc := e.NewAccount(t)

	// No account = null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value(), "expected null for non-existent career")
	}, "getActiveCareer", acc.ScriptHash())
}

// TestSese_GetActiveSabbatical_NoAccount tests getting active sabbatical for non-existent account.
func TestSese_GetActiveSabbatical_NoAccount(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	acc := e.NewAccount(t)

	// No account = null
	c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		require.Nil(t, stack[0].Value(), "expected null for non-existent sabbatical")
	}, "getActiveSabbatical", acc.ScriptHash())
}

// TestSese_ActivateLifePlanWithVita tests life plan activation with a valid Vita.
func TestSese_ActivateLifePlanWithVita(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	// Register Vita first
	vitaHash := e.NativeHash(t, nativenames.Vita)
	acc := e.NewAccount(t)
	vitaInvoker := e.NewInvoker(vitaHash, acc)

	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

	// Register Vita token
	vitaInvoker.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)

	// Now activate Sese account - need to pass owner as BytesBE for Hash160 type
	seseInvoker := c.WithSigners(acc)
	seseInvoker.Invoke(t, true, "activateLifePlan", owner.BytesBE())

	// Verify account exists - also pass as BytesBE
	seseInvoker.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 result for existing account")
		require.GreaterOrEqual(t, len(arr), 14) // LifePlanAccount has 14 fields
	}, "getAccount", owner.BytesBE())

	// Verify total accounts increased
	c.Invoke(t, 1, "getTotalAccounts")

	// Verify default sabbatical credits (5000)
	c.Invoke(t, 5000, "getSabbaticalCredits", owner.BytesBE())
}

// TestSese_Contribute tests contribution to life plan.
func TestSese_Contribute(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	// Register Vita first
	vitaHash := e.NativeHash(t, nativenames.Vita)
	acc := e.NewAccount(t)
	vitaInvoker := e.NewInvoker(vitaHash, acc)

	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

	// Register Vita token
	vitaInvoker.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)

	// Activate Sese account
	seseInvoker := c.WithSigners(acc)
	seseInvoker.Invoke(t, true, "activateLifePlan", owner.BytesBE())

	// Contribute 1000
	seseInvoker.Invoke(t, true, "contribute", owner.BytesBE(), int64(1000), false)

	// Balance should be 1000 + 500 (50% gov match) = 1500
	c.Invoke(t, 1500, "getBalance", owner.BytesBE())
}

// TestSese_StartCareer tests starting a career.
func TestSese_StartCareer(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	// Register Vita first
	vitaHash := e.NativeHash(t, nativenames.Vita)
	acc := e.NewAccount(t)
	vitaInvoker := e.NewInvoker(vitaHash, acc)

	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

	// Register Vita token
	vitaInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
	}, "register", owner.BytesBE(), personHash, isEntity, recoveryHash, birthTimestamp)

	// Activate Sese account
	seseInvoker := c.WithSigners(acc)
	seseInvoker.Invoke(t, true, "activateLifePlan", owner.BytesBE())

	// Start career
	seseInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		// Career ID should be returned (0 for first career)
	}, "startCareer", owner.BytesBE(), "Software Engineering")

	// Total careers should be 1
	c.Invoke(t, 1, "getTotalCareers")

	// Get active career
	seseInvoker.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 result for career")
		require.GreaterOrEqual(t, len(arr), 9) // CareerCycle has 9 fields
	}, "getActiveCareer", owner.BytesBE())
}

// TestSese_CreateGoal tests creating a life goal.
func TestSese_CreateGoal(t *testing.T) {
	c := newSeseClient(t)
	e := c.Executor

	// Register Vita first
	vitaHash := e.NativeHash(t, nativenames.Vita)
	acc := e.NewAccount(t)
	vitaInvoker := e.NewInvoker(vitaHash, acc)

	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

	// Register Vita token
	vitaInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
	}, "register", owner.BytesBE(), personHash, isEntity, recoveryHash, birthTimestamp)

	// Activate Sese account
	seseInvoker := c.WithSigners(acc)
	seseInvoker.Invoke(t, true, "activateLifePlan", owner.BytesBE())

	// Create goal
	seseInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
		require.Equal(t, 1, len(stack))
		// Goal ID should be returned (0 for first goal)
	}, "createGoal", owner.BytesBE(), "Learn Blockchain", "Master blockchain development", int64(1000000))

	// Total goals should be 1
	c.Invoke(t, 1, "getTotalGoals")

	// Get goal
	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 result for goal")
		require.GreaterOrEqual(t, len(arr), 10) // LifeGoal has 10 fields
	}, "getGoal", int64(0))
}
