package repositories

import (
	"testing"
	"time"

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

	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/testutil"
)

// === Password Reset Code Lifecycle ===

func TestUserRepository_PasswordResetCode_Lifecycle(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	code, err := repo.CreatePasswordResetCode(user.ID, "hash_abc123", "reset_token_xyz", expiresAt)
	require.NoError(t, err)
	assert.NotZero(t, code.ID)
	assert.Equal(t, "hash_abc123", code.CodeHash)
	assert.Equal(t, "reset_token_xyz", code.ResetToken)
	assert.False(t, code.Used)
	assert.Equal(t, 0, code.Attempts)
}

func TestUserRepository_CreatePasswordResetCode_InvalidatesExisting(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)

	code1, err := repo.CreatePasswordResetCode(user.ID, "hash1", "token1", expiresAt)
	require.NoError(t, err)

	_, err = repo.CreatePasswordResetCode(user.ID, "hash2", "token2", expiresAt)
	require.NoError(t, err)

	// First code should be marked as used
	var c models.PasswordResetCode
	db.First(&c, code1.ID)
	assert.True(t, c.Used)
}

func TestUserRepository_FindPasswordResetCodeByEmail(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	_, err := repo.CreatePasswordResetCode(user.ID, "hash_abc", "token_abc", expiresAt)
	require.NoError(t, err)

	found, foundUser, err := repo.FindPasswordResetCodeByEmail("test@example.com")
	require.NoError(t, err)
	assert.Equal(t, user.ID, foundUser.ID)
	assert.Equal(t, "hash_abc", found.CodeHash)
}

func TestUserRepository_FindPasswordResetCodeByEmail_UserNotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	_, _, err := repo.FindPasswordResetCodeByEmail("nonexistent@example.com")
	assert.Error(t, err)
}

func TestUserRepository_FindPasswordResetCodeByEmail_NoCode(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	_, _, err := repo.FindPasswordResetCodeByEmail("test@example.com")
	assert.ErrorIs(t, err, ErrCodeNotFound)
}

func TestUserRepository_FindPasswordResetCodeByToken(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	_, err := repo.CreatePasswordResetCode(user.ID, "hash_xyz", "token_xyz", expiresAt)
	require.NoError(t, err)

	found, err := repo.FindPasswordResetCodeByToken("token_xyz")
	require.NoError(t, err)
	assert.Equal(t, "hash_xyz", found.CodeHash)
}

func TestUserRepository_FindPasswordResetCodeByToken_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	_, err := repo.FindPasswordResetCodeByToken("nonexistent_token")
	assert.ErrorIs(t, err, ErrCodeNotFound)
}

func TestUserRepository_FindPasswordResetCodeByToken_Expired(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	// Already expired
	expiresAt := time.Now().UTC().Add(-1 * time.Hour)
	_, err := repo.CreatePasswordResetCode(user.ID, "hash_exp", "token_exp", expiresAt)
	require.NoError(t, err)

	_, err = repo.FindPasswordResetCodeByToken("token_exp")
	assert.ErrorIs(t, err, ErrCodeExpired)
}

func TestUserRepository_FindPasswordResetCodeByToken_Used(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	code, err := repo.CreatePasswordResetCode(user.ID, "hash_used", "token_used", expiresAt)
	require.NoError(t, err)

	// Mark as used
	err = repo.MarkPasswordResetCodeUsed(code.ID)
	require.NoError(t, err)

	_, err = repo.FindPasswordResetCodeByToken("token_used")
	assert.ErrorIs(t, err, ErrCodeUsed)
}

func TestUserRepository_FindPasswordResetCodeByToken_TooManyAttempts(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	code, err := repo.CreatePasswordResetCode(user.ID, "hash_attempts", "token_attempts", expiresAt)
	require.NoError(t, err)

	// Max out attempts
	for i := 0; i < 5; i++ {
		err = repo.IncrementResetCodeAttempts(code.ID)
		require.NoError(t, err)
	}

	_, err = repo.FindPasswordResetCodeByToken("token_attempts")
	assert.ErrorIs(t, err, ErrTooManyAttempts)
}

func TestUserRepository_IncrementResetCodeAttempts(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	code, err := repo.CreatePasswordResetCode(user.ID, "hash_inc", "token_inc", expiresAt)
	require.NoError(t, err)

	err = repo.IncrementResetCodeAttempts(code.ID)
	require.NoError(t, err)

	var updated models.PasswordResetCode
	db.First(&updated, code.ID)
	assert.Equal(t, 1, updated.Attempts)
}

func TestUserRepository_MarkPasswordResetCodeUsed(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	code, err := repo.CreatePasswordResetCode(user.ID, "hash_mark", "token_mark", expiresAt)
	require.NoError(t, err)

	err = repo.MarkPasswordResetCodeUsed(code.ID)
	require.NoError(t, err)

	var updated models.PasswordResetCode
	db.First(&updated, code.ID)
	assert.True(t, updated.Used)
}

func TestUserRepository_CountRecentPasswordResetRequests(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	expiresAt := time.Now().UTC().Add(1 * time.Hour)
	_, err := repo.CreatePasswordResetCode(user.ID, "hash1", "token1", expiresAt)
	require.NoError(t, err)
	_, err = repo.CreatePasswordResetCode(user.ID, "hash2", "token2", expiresAt)
	require.NoError(t, err)

	count, err := repo.CountRecentPasswordResetRequests(user.ID)
	require.NoError(t, err)
	assert.Equal(t, int64(2), count)
}

// === FindUsersInSharedResidences ===

func TestUserRepository_FindUsersInSharedResidences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	userRepo := NewUserRepository(db)
	resRepo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")
	unrelated := testutil.CreateTestUser(t, db, "unrelated", "unrelated@test.com", "Password123")

	residence := testutil.CreateTestResidence(t, db, owner.ID, "Shared House")
	resRepo.AddUser(residence.ID, shared.ID)

	// Owner should see shared user
	users, err := userRepo.FindUsersInSharedResidences(owner.ID)
	require.NoError(t, err)
	assert.Len(t, users, 1)
	assert.Equal(t, shared.ID, users[0].ID)

	// Shared user should see owner
	users, err = userRepo.FindUsersInSharedResidences(shared.ID)
	require.NoError(t, err)
	assert.Len(t, users, 1)
	assert.Equal(t, owner.ID, users[0].ID)

	// Unrelated should see no one
	users, err = userRepo.FindUsersInSharedResidences(unrelated.ID)
	require.NoError(t, err)
	assert.Empty(t, users)
}

// === FindUserIfSharedResidence ===

func TestUserRepository_FindUserIfSharedResidence(t *testing.T) {
	db := testutil.SetupTestDB(t)
	userRepo := NewUserRepository(db)
	resRepo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")
	unrelated := testutil.CreateTestUser(t, db, "unrelated", "unrelated@test.com", "Password123")

	residence := testutil.CreateTestResidence(t, db, owner.ID, "Shared House")
	resRepo.AddUser(residence.ID, shared.ID)

	// Owner requesting shared user => should find
	found, err := userRepo.FindUserIfSharedResidence(shared.ID, owner.ID)
	require.NoError(t, err)
	require.NotNil(t, found)
	assert.Equal(t, shared.ID, found.ID)

	// Unrelated requesting shared user => should not find
	found, err = userRepo.FindUserIfSharedResidence(shared.ID, unrelated.ID)
	require.NoError(t, err)
	assert.Nil(t, found)

	// Requesting self => should work
	found, err = userRepo.FindUserIfSharedResidence(owner.ID, owner.ID)
	require.NoError(t, err)
	require.NotNil(t, found)
	assert.Equal(t, owner.ID, found.ID)
}

// === FindProfilesInSharedResidences ===

func TestUserRepository_FindProfilesInSharedResidences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	userRepo := NewUserRepository(db)
	resRepo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
	shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")

	// Create profiles
	ownerProfile := &models.UserProfile{UserID: owner.ID, Bio: "Owner bio"}
	sharedProfile := &models.UserProfile{UserID: shared.ID, Bio: "Shared bio"}
	require.NoError(t, db.Create(ownerProfile).Error)
	require.NoError(t, db.Create(sharedProfile).Error)

	residence := testutil.CreateTestResidence(t, db, owner.ID, "Shared House")
	resRepo.AddUser(residence.ID, shared.ID)

	// Owner sees own profile + shared user profile
	profiles, err := userRepo.FindProfilesInSharedResidences(owner.ID)
	require.NoError(t, err)
	assert.Len(t, profiles, 2)
}

// === ConfirmationCode Expired ===

func TestUserRepository_FindConfirmationCode_Expired(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	// Create already-expired code
	expiresAt := time.Now().UTC().Add(-1 * time.Hour)
	_, err := repo.CreateConfirmationCode(user.ID, "999999", expiresAt)
	require.NoError(t, err)

	_, err = repo.FindConfirmationCode(user.ID, "999999")
	assert.ErrorIs(t, err, ErrCodeExpired)
}

func TestUserRepository_FindConfirmationCode_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	_, err := repo.FindConfirmationCode(user.ID, "000000")
	assert.ErrorIs(t, err, ErrCodeNotFound)
}

// === Transaction Rollback ===

func TestUserRepository_Transaction_Rollback(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123")

	err := repo.Transaction(func(txRepo *UserRepository) error {
		found, err := txRepo.FindByID(user.ID)
		if err != nil {
			return err
		}
		found.FirstName = "ShouldRollback"
		if err := txRepo.Update(found); err != nil {
			return err
		}
		// Simulate an error to trigger rollback
		return ErrUserNotFound
	})
	assert.Error(t, err)

	// Name should NOT have been updated
	found, err := repo.FindByID(user.ID)
	require.NoError(t, err)
	assert.NotEqual(t, "ShouldRollback", found.FirstName)
}

// === FindByUsernameOrEmail not found ===

func TestUserRepository_FindByUsernameOrEmail_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewUserRepository(db)

	_, err := repo.FindByUsernameOrEmail("nonexistent")
	assert.ErrorIs(t, err, ErrUserNotFound)
}
