//go:build integration

package integration_test

import (
	"context"
	"database/sql"
	"os"
	"testing"

	"github.com/google/uuid"
	_ "github.com/lib/pq"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

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

type AccountRepositoryTestSuite struct {
	suite.Suite
	db           *sql.DB
	accountRepo  *postgres.AccountPostgresRepo
	shareRepo    *postgres.AccountSharePostgresRepo
	recoveryRepo *postgres.RecoverySessionPostgresRepo
	ctx          context.Context
}

func TestAccountRepositorySuite(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}
	suite.Run(t, new(AccountRepositoryTestSuite))
}

func (s *AccountRepositoryTestSuite) SetupSuite() {
	dsn := os.Getenv("TEST_DATABASE_URL")
	if dsn == "" {
		dsn = "postgres://mpc_user:mpc_password@localhost:5433/mpc_system_test?sslmode=disable"
	}

	var err error
	s.db, err = sql.Open("postgres", dsn)
	require.NoError(s.T(), err)

	err = s.db.Ping()
	require.NoError(s.T(), err, "Failed to connect to test database")

	s.accountRepo = postgres.NewAccountPostgresRepo(s.db).(*postgres.AccountPostgresRepo)
	s.shareRepo = postgres.NewAccountSharePostgresRepo(s.db).(*postgres.AccountSharePostgresRepo)
	s.recoveryRepo = postgres.NewRecoverySessionPostgresRepo(s.db).(*postgres.RecoverySessionPostgresRepo)
	s.ctx = context.Background()
}

func (s *AccountRepositoryTestSuite) TearDownSuite() {
	if s.db != nil {
		s.db.Close()
	}
}

func (s *AccountRepositoryTestSuite) SetupTest() {
	s.cleanupTestData()
}

func (s *AccountRepositoryTestSuite) cleanupTestData() {
	s.db.ExecContext(s.ctx, "DELETE FROM account_recovery_sessions WHERE account_id IN (SELECT id FROM accounts WHERE username LIKE 'test_%')")
	s.db.ExecContext(s.ctx, "DELETE FROM account_shares WHERE account_id IN (SELECT id FROM accounts WHERE username LIKE 'test_%')")
	s.db.ExecContext(s.ctx, "DELETE FROM accounts WHERE username LIKE 'test_%'")
}

func (s *AccountRepositoryTestSuite) TestCreateAccount() {
	account := entities.NewAccount(
		"test_user_1",
		"test1@example.com",
		[]byte("test-public-key-1"),
		uuid.New(),
		3,
		2,
	)

	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	// Verify account was created
	retrieved, err := s.accountRepo.GetByID(s.ctx, account.ID)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), account.Username, retrieved.Username)
	assert.Equal(s.T(), account.Email, retrieved.Email)
	assert.Equal(s.T(), account.ThresholdN, retrieved.ThresholdN)
	assert.Equal(s.T(), account.ThresholdT, retrieved.ThresholdT)
}

func (s *AccountRepositoryTestSuite) TestGetByUsername() {
	account := entities.NewAccount(
		"test_user_2",
		"test2@example.com",
		[]byte("test-public-key-2"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	retrieved, err := s.accountRepo.GetByUsername(s.ctx, "test_user_2")
	require.NoError(s.T(), err)
	assert.True(s.T(), account.ID.Equals(retrieved.ID))
}

func (s *AccountRepositoryTestSuite) TestGetByEmail() {
	account := entities.NewAccount(
		"test_user_3",
		"test3@example.com",
		[]byte("test-public-key-3"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	retrieved, err := s.accountRepo.GetByEmail(s.ctx, "test3@example.com")
	require.NoError(s.T(), err)
	assert.True(s.T(), account.ID.Equals(retrieved.ID))
}

func (s *AccountRepositoryTestSuite) TestUpdateAccount() {
	account := entities.NewAccount(
		"test_user_4",
		"test4@example.com",
		[]byte("test-public-key-4"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	// Update account
	phone := "+1234567890"
	account.Phone = &phone
	account.Status = value_objects.AccountStatusSuspended

	err = s.accountRepo.Update(s.ctx, account)
	require.NoError(s.T(), err)

	// Verify update
	retrieved, err := s.accountRepo.GetByID(s.ctx, account.ID)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), "+1234567890", *retrieved.Phone)
	assert.Equal(s.T(), value_objects.AccountStatusSuspended, retrieved.Status)
}

func (s *AccountRepositoryTestSuite) TestExistsByUsername() {
	account := entities.NewAccount(
		"test_user_5",
		"test5@example.com",
		[]byte("test-public-key-5"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	exists, err := s.accountRepo.ExistsByUsername(s.ctx, "test_user_5")
	require.NoError(s.T(), err)
	assert.True(s.T(), exists)

	exists, err = s.accountRepo.ExistsByUsername(s.ctx, "nonexistent_user")
	require.NoError(s.T(), err)
	assert.False(s.T(), exists)
}

func (s *AccountRepositoryTestSuite) TestExistsByEmail() {
	account := entities.NewAccount(
		"test_user_6",
		"test6@example.com",
		[]byte("test-public-key-6"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	exists, err := s.accountRepo.ExistsByEmail(s.ctx, "test6@example.com")
	require.NoError(s.T(), err)
	assert.True(s.T(), exists)

	exists, err = s.accountRepo.ExistsByEmail(s.ctx, "nonexistent@example.com")
	require.NoError(s.T(), err)
	assert.False(s.T(), exists)
}

func (s *AccountRepositoryTestSuite) TestListAccounts() {
	// Create multiple accounts
	for i := 0; i < 5; i++ {
		account := entities.NewAccount(
			"test_user_list_"+string(rune('a'+i)),
			"testlist"+string(rune('a'+i))+"@example.com",
			[]byte("test-public-key-list-"+string(rune('a'+i))),
			uuid.New(),
			3,
			2,
		)
		err := s.accountRepo.Create(s.ctx, account)
		require.NoError(s.T(), err)
	}

	accounts, err := s.accountRepo.List(s.ctx, 0, 10)
	require.NoError(s.T(), err)
	assert.GreaterOrEqual(s.T(), len(accounts), 5)

	count, err := s.accountRepo.Count(s.ctx)
	require.NoError(s.T(), err)
	assert.GreaterOrEqual(s.T(), count, int64(5))
}

func (s *AccountRepositoryTestSuite) TestDeleteAccount() {
	account := entities.NewAccount(
		"test_user_delete",
		"testdelete@example.com",
		[]byte("test-public-key-delete"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	err = s.accountRepo.Delete(s.ctx, account.ID)
	require.NoError(s.T(), err)

	_, err = s.accountRepo.GetByID(s.ctx, account.ID)
	assert.Error(s.T(), err)
}

// Account Share Tests

func (s *AccountRepositoryTestSuite) TestCreateAccountShare() {
	account := entities.NewAccount(
		"test_user_share_1",
		"testshare1@example.com",
		[]byte("test-public-key-share-1"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	share := entities.NewAccountShare(
		account.ID,
		value_objects.ShareTypeUserDevice,
		"party_1",
		0,
	)
	share.SetDeviceInfo("iOS", "device123")

	err = s.shareRepo.Create(s.ctx, share)
	require.NoError(s.T(), err)

	// Verify share was created
	retrieved, err := s.shareRepo.GetByID(s.ctx, share.ID.String())
	require.NoError(s.T(), err)
	assert.Equal(s.T(), share.PartyID, retrieved.PartyID)
	assert.Equal(s.T(), "iOS", *retrieved.DeviceType)
}

func (s *AccountRepositoryTestSuite) TestGetSharesByAccountID() {
	account := entities.NewAccount(
		"test_user_share_2",
		"testshare2@example.com",
		[]byte("test-public-key-share-2"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	// Create multiple shares
	shareTypes := []value_objects.ShareType{
		value_objects.ShareTypeUserDevice,
		value_objects.ShareTypeServer,
		value_objects.ShareTypeRecovery,
	}

	for i, st := range shareTypes {
		share := entities.NewAccountShare(account.ID, st, "party_"+string(rune('a'+i)), i)
		err = s.shareRepo.Create(s.ctx, share)
		require.NoError(s.T(), err)
	}

	shares, err := s.shareRepo.GetByAccountID(s.ctx, account.ID)
	require.NoError(s.T(), err)
	assert.Len(s.T(), shares, 3)
}

func (s *AccountRepositoryTestSuite) TestGetActiveSharesByAccountID() {
	account := entities.NewAccount(
		"test_user_share_3",
		"testshare3@example.com",
		[]byte("test-public-key-share-3"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	// Create active and inactive shares
	activeShare := entities.NewAccountShare(account.ID, value_objects.ShareTypeUserDevice, "party_active", 0)
	err = s.shareRepo.Create(s.ctx, activeShare)
	require.NoError(s.T(), err)

	inactiveShare := entities.NewAccountShare(account.ID, value_objects.ShareTypeServer, "party_inactive", 1)
	inactiveShare.Deactivate()
	err = s.shareRepo.Create(s.ctx, inactiveShare)
	require.NoError(s.T(), err)

	activeShares, err := s.shareRepo.GetActiveByAccountID(s.ctx, account.ID)
	require.NoError(s.T(), err)
	assert.Len(s.T(), activeShares, 1)
	assert.Equal(s.T(), "party_active", activeShares[0].PartyID)
}

func (s *AccountRepositoryTestSuite) TestDeactivateShareByAccountID() {
	account := entities.NewAccount(
		"test_user_share_4",
		"testshare4@example.com",
		[]byte("test-public-key-share-4"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	share1 := entities.NewAccountShare(account.ID, value_objects.ShareTypeUserDevice, "party_1", 0)
	share2 := entities.NewAccountShare(account.ID, value_objects.ShareTypeServer, "party_2", 1)
	err = s.shareRepo.Create(s.ctx, share1)
	require.NoError(s.T(), err)
	err = s.shareRepo.Create(s.ctx, share2)
	require.NoError(s.T(), err)

	// Deactivate all shares
	err = s.shareRepo.DeactivateByAccountID(s.ctx, account.ID)
	require.NoError(s.T(), err)

	activeShares, err := s.shareRepo.GetActiveByAccountID(s.ctx, account.ID)
	require.NoError(s.T(), err)
	assert.Len(s.T(), activeShares, 0)
}

// Recovery Session Tests

func (s *AccountRepositoryTestSuite) TestCreateRecoverySession() {
	account := entities.NewAccount(
		"test_user_recovery_1",
		"testrecovery1@example.com",
		[]byte("test-public-key-recovery-1"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	recovery := entities.NewRecoverySession(account.ID, value_objects.RecoveryTypeDeviceLost)
	oldShareType := value_objects.ShareTypeUserDevice
	recovery.SetOldShareType(oldShareType)

	err = s.recoveryRepo.Create(s.ctx, recovery)
	require.NoError(s.T(), err)

	// Verify recovery was created
	retrieved, err := s.recoveryRepo.GetByID(s.ctx, recovery.ID.String())
	require.NoError(s.T(), err)
	assert.Equal(s.T(), recovery.RecoveryType, retrieved.RecoveryType)
	assert.Equal(s.T(), value_objects.RecoveryStatusRequested, retrieved.Status)
}

func (s *AccountRepositoryTestSuite) TestUpdateRecoverySession() {
	account := entities.NewAccount(
		"test_user_recovery_2",
		"testrecovery2@example.com",
		[]byte("test-public-key-recovery-2"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	recovery := entities.NewRecoverySession(account.ID, value_objects.RecoveryTypeDeviceLost)
	err = s.recoveryRepo.Create(s.ctx, recovery)
	require.NoError(s.T(), err)

	// Start keygen
	keygenID := uuid.New()
	recovery.StartKeygen(keygenID)
	err = s.recoveryRepo.Update(s.ctx, recovery)
	require.NoError(s.T(), err)

	// Verify update
	retrieved, err := s.recoveryRepo.GetByID(s.ctx, recovery.ID.String())
	require.NoError(s.T(), err)
	assert.Equal(s.T(), value_objects.RecoveryStatusInProgress, retrieved.Status)
	assert.NotNil(s.T(), retrieved.NewKeygenSessionID)
}

func (s *AccountRepositoryTestSuite) TestGetActiveRecoveryByAccountID() {
	account := entities.NewAccount(
		"test_user_recovery_3",
		"testrecovery3@example.com",
		[]byte("test-public-key-recovery-3"),
		uuid.New(),
		3,
		2,
	)
	err := s.accountRepo.Create(s.ctx, account)
	require.NoError(s.T(), err)

	// Create active recovery
	activeRecovery := entities.NewRecoverySession(account.ID, value_objects.RecoveryTypeDeviceLost)
	err = s.recoveryRepo.Create(s.ctx, activeRecovery)
	require.NoError(s.T(), err)

	retrieved, err := s.recoveryRepo.GetActiveByAccountID(s.ctx, account.ID)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), activeRecovery.ID, retrieved.ID)
}
