package domain_test

import (
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/rwadurian/mpc-system/services/account/domain/entities"
	"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
)

func TestNewAccount(t *testing.T) {
	t.Run("should create account with valid data", func(t *testing.T) {
		publicKey := []byte("test-public-key")
		keygenSessionID := uuid.New()

		account := entities.NewAccount(
			"testuser",
			"test@example.com",
			publicKey,
			keygenSessionID,
			3, // thresholdN
			2, // thresholdT
		)

		assert.NotNil(t, account)
		assert.False(t, account.ID.IsZero())
		assert.Equal(t, "testuser", account.Username)
		assert.Equal(t, "test@example.com", account.Email)
		assert.Equal(t, publicKey, account.PublicKey)
		assert.Equal(t, keygenSessionID, account.KeygenSessionID)
		assert.Equal(t, 3, account.ThresholdN)
		assert.Equal(t, 2, account.ThresholdT)
		assert.Equal(t, value_objects.AccountStatusActive, account.Status)
		assert.True(t, account.CreatedAt.Before(time.Now().Add(time.Second)))
	})
}

func TestAccount_SetPhone(t *testing.T) {
	t.Run("should set phone number", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)

		account.SetPhone("+1234567890")

		assert.NotNil(t, account.Phone)
		assert.Equal(t, "+1234567890", *account.Phone)
	})
}

func TestAccount_UpdateLastLogin(t *testing.T) {
	t.Run("should update last login timestamp", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
		assert.Nil(t, account.LastLoginAt)

		account.UpdateLastLogin()

		assert.NotNil(t, account.LastLoginAt)
		assert.True(t, account.LastLoginAt.After(account.CreatedAt.Add(-time.Second)))
	})
}

func TestAccount_Suspend(t *testing.T) {
	t.Run("should suspend active account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)

		err := account.Suspend()

		require.NoError(t, err)
		assert.Equal(t, value_objects.AccountStatusSuspended, account.Status)
	})

	t.Run("should fail to suspend recovering account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
		account.Status = value_objects.AccountStatusRecovering

		err := account.Suspend()

		assert.Error(t, err)
		assert.Equal(t, entities.ErrAccountInRecovery, err)
	})
}

func TestAccount_Lock(t *testing.T) {
	t.Run("should lock active account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)

		err := account.Lock()

		require.NoError(t, err)
		assert.Equal(t, value_objects.AccountStatusLocked, account.Status)
	})

	t.Run("should fail to lock recovering account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
		account.Status = value_objects.AccountStatusRecovering

		err := account.Lock()

		assert.Error(t, err)
	})
}

func TestAccount_Activate(t *testing.T) {
	t.Run("should activate suspended account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
		account.Status = value_objects.AccountStatusSuspended

		account.Activate()

		assert.Equal(t, value_objects.AccountStatusActive, account.Status)
	})
}

func TestAccount_StartRecovery(t *testing.T) {
	t.Run("should start recovery for active account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)

		err := account.StartRecovery()

		require.NoError(t, err)
		assert.Equal(t, value_objects.AccountStatusRecovering, account.Status)
	})

	t.Run("should start recovery for locked account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
		account.Status = value_objects.AccountStatusLocked

		err := account.StartRecovery()

		require.NoError(t, err)
		assert.Equal(t, value_objects.AccountStatusRecovering, account.Status)
	})

	t.Run("should fail to start recovery for suspended account", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
		account.Status = value_objects.AccountStatusSuspended

		err := account.StartRecovery()

		assert.Error(t, err)
	})
}

func TestAccount_CompleteRecovery(t *testing.T) {
	t.Run("should complete recovery with new public key", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("old-key"), uuid.New(), 3, 2)
		account.Status = value_objects.AccountStatusRecovering

		newPublicKey := []byte("new-public-key")
		newKeygenSessionID := uuid.New()

		account.CompleteRecovery(newPublicKey, newKeygenSessionID)

		assert.Equal(t, value_objects.AccountStatusActive, account.Status)
		assert.Equal(t, newPublicKey, account.PublicKey)
		assert.Equal(t, newKeygenSessionID, account.KeygenSessionID)
	})
}

func TestAccount_CanLogin(t *testing.T) {
	testCases := []struct {
		name     string
		status   value_objects.AccountStatus
		canLogin bool
	}{
		{"active account can login", value_objects.AccountStatusActive, true},
		{"suspended account cannot login", value_objects.AccountStatusSuspended, false},
		{"locked account cannot login", value_objects.AccountStatusLocked, false},
		{"recovering account cannot login", value_objects.AccountStatusRecovering, false},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
			account.Status = tc.status

			assert.Equal(t, tc.canLogin, account.CanLogin())
		})
	}
}

func TestAccount_Validate(t *testing.T) {
	t.Run("should pass validation with valid data", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)

		err := account.Validate()

		assert.NoError(t, err)
	})

	t.Run("should fail validation with empty username", func(t *testing.T) {
		account := entities.NewAccount("", "user@test.com", []byte("key"), uuid.New(), 3, 2)

		err := account.Validate()

		assert.Error(t, err)
		assert.Equal(t, entities.ErrInvalidUsername, err)
	})

	t.Run("should fail validation with empty email", func(t *testing.T) {
		account := entities.NewAccount("user", "", []byte("key"), uuid.New(), 3, 2)

		err := account.Validate()

		assert.Error(t, err)
		assert.Equal(t, entities.ErrInvalidEmail, err)
	})

	t.Run("should fail validation with empty public key", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte{}, uuid.New(), 3, 2)

		err := account.Validate()

		assert.Error(t, err)
		assert.Equal(t, entities.ErrInvalidPublicKey, err)
	})

	t.Run("should fail validation with invalid threshold", func(t *testing.T) {
		account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 2, 3) // t > n

		err := account.Validate()

		assert.Error(t, err)
		assert.Equal(t, entities.ErrInvalidThreshold, err)
	})
}

func TestAccountID(t *testing.T) {
	t.Run("should create new account ID", func(t *testing.T) {
		id := value_objects.NewAccountID()
		assert.False(t, id.IsZero())
	})

	t.Run("should create account ID from string", func(t *testing.T) {
		original := value_objects.NewAccountID()
		parsed, err := value_objects.AccountIDFromString(original.String())
		require.NoError(t, err)
		assert.True(t, original.Equals(parsed))
	})

	t.Run("should fail to parse invalid account ID", func(t *testing.T) {
		_, err := value_objects.AccountIDFromString("invalid-uuid")
		assert.Error(t, err)
	})
}

func TestAccountStatus(t *testing.T) {
	t.Run("should validate status correctly", func(t *testing.T) {
		validStatuses := []value_objects.AccountStatus{
			value_objects.AccountStatusActive,
			value_objects.AccountStatusSuspended,
			value_objects.AccountStatusLocked,
			value_objects.AccountStatusRecovering,
		}

		for _, status := range validStatuses {
			assert.True(t, status.IsValid(), "status %s should be valid", status)
		}

		invalidStatus := value_objects.AccountStatus("invalid")
		assert.False(t, invalidStatus.IsValid())
	})
}

func TestShareType(t *testing.T) {
	t.Run("should validate share type correctly", func(t *testing.T) {
		validTypes := []value_objects.ShareType{
			value_objects.ShareTypeUserDevice,
			value_objects.ShareTypeServer,
			value_objects.ShareTypeRecovery,
		}

		for _, st := range validTypes {
			assert.True(t, st.IsValid(), "share type %s should be valid", st)
		}

		invalidType := value_objects.ShareType("invalid")
		assert.False(t, invalidType.IsValid())
	})
}

func TestAccountShare(t *testing.T) {
	t.Run("should create account share with correct initial state", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		share := entities.NewAccountShare(
			accountID,
			value_objects.ShareTypeUserDevice,
			"party1",
			0,
		)

		assert.NotEqual(t, uuid.Nil, share.ID)
		assert.True(t, share.AccountID.Equals(accountID))
		assert.Equal(t, value_objects.ShareTypeUserDevice, share.ShareType)
		assert.Equal(t, "party1", share.PartyID)
		assert.Equal(t, 0, share.PartyIndex)
		assert.True(t, share.IsActive)
	})

	t.Run("should set device info", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "party1", 0)

		share.SetDeviceInfo("iOS", "device123")

		assert.NotNil(t, share.DeviceType)
		assert.Equal(t, "iOS", *share.DeviceType)
		assert.NotNil(t, share.DeviceID)
		assert.Equal(t, "device123", *share.DeviceID)
	})

	t.Run("should deactivate share", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "party1", 0)

		share.Deactivate()

		assert.False(t, share.IsActive)
	})

	t.Run("should identify share types correctly", func(t *testing.T) {
		accountID := value_objects.NewAccountID()

		userShare := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "p1", 0)
		serverShare := entities.NewAccountShare(accountID, value_objects.ShareTypeServer, "p2", 1)
		recoveryShare := entities.NewAccountShare(accountID, value_objects.ShareTypeRecovery, "p3", 2)

		assert.True(t, userShare.IsUserDeviceShare())
		assert.False(t, userShare.IsServerShare())

		assert.True(t, serverShare.IsServerShare())
		assert.False(t, serverShare.IsUserDeviceShare())

		assert.True(t, recoveryShare.IsRecoveryShare())
		assert.False(t, recoveryShare.IsServerShare())
	})

	t.Run("should validate share correctly", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "party1", 0)

		err := share.Validate()
		assert.NoError(t, err)
	})

	t.Run("should fail validation with empty party ID", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "", 0)

		err := share.Validate()
		assert.Error(t, err)
	})
}

func TestRecoverySession(t *testing.T) {
	t.Run("should create recovery session with correct initial state", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)

		assert.NotEqual(t, uuid.Nil, session.ID)
		assert.True(t, session.AccountID.Equals(accountID))
		assert.Equal(t, value_objects.RecoveryTypeDeviceLost, session.RecoveryType)
		assert.Equal(t, value_objects.RecoveryStatusRequested, session.Status)
	})

	t.Run("should start keygen", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
		keygenID := uuid.New()

		err := session.StartKeygen(keygenID)

		require.NoError(t, err)
		assert.Equal(t, value_objects.RecoveryStatusInProgress, session.Status)
		assert.NotNil(t, session.NewKeygenSessionID)
		assert.Equal(t, keygenID, *session.NewKeygenSessionID)
	})

	t.Run("should complete recovery", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
		session.StartKeygen(uuid.New())

		err := session.Complete()

		require.NoError(t, err)
		assert.Equal(t, value_objects.RecoveryStatusCompleted, session.Status)
		assert.NotNil(t, session.CompletedAt)
	})

	t.Run("should fail recovery", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)

		err := session.Fail()

		require.NoError(t, err)
		assert.Equal(t, value_objects.RecoveryStatusFailed, session.Status)
	})

	t.Run("should not complete already completed recovery", func(t *testing.T) {
		accountID := value_objects.NewAccountID()
		session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
		session.StartKeygen(uuid.New())
		session.Complete()

		err := session.Fail()

		assert.Error(t, err)
	})
}
