package repositories

import (
	"testing"

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

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

func TestNotificationRepository_Create(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	notification := &models.Notification{
		UserID:           user.ID,
		NotificationType: models.NotificationTaskDueSoon,
		Title:            "Task Due Soon",
		Body:             "Your task is due tomorrow",
	}

	err := repo.Create(notification)
	require.NoError(t, err)
	assert.NotZero(t, notification.ID)
}

func TestNotificationRepository_FindByID(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	notification := &models.Notification{
		UserID:           user.ID,
		NotificationType: models.NotificationTaskOverdue,
		Title:            "Task Overdue",
		Body:             "Your task is overdue",
	}
	err := db.Create(notification).Error
	require.NoError(t, err)

	found, err := repo.FindByID(notification.ID)
	require.NoError(t, err)
	assert.Equal(t, "Task Overdue", found.Title)
	assert.Equal(t, user.ID, found.UserID)
}

func TestNotificationRepository_FindByID_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

	_, err := repo.FindByID(9999)
	assert.Error(t, err)
}

func TestNotificationRepository_FindByUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create notifications for user
	for i := 0; i < 5; i++ {
		n := &models.Notification{
			UserID:           user.ID,
			NotificationType: models.NotificationTaskDueSoon,
			Title:            "Notification",
			Body:             "Body",
		}
		err := db.Create(n).Error
		require.NoError(t, err)
	}

	// Create notification for other user
	otherN := &models.Notification{
		UserID:           otherUser.ID,
		NotificationType: models.NotificationTaskDueSoon,
		Title:            "Other",
		Body:             "Body",
	}
	err := db.Create(otherN).Error
	require.NoError(t, err)

	// Find all for user
	notifications, err := repo.FindByUser(user.ID, 0, 0)
	require.NoError(t, err)
	assert.Len(t, notifications, 5)

	// Find with limit
	notifications, err = repo.FindByUser(user.ID, 3, 0)
	require.NoError(t, err)
	assert.Len(t, notifications, 3)

	// Find with offset
	notifications, err = repo.FindByUser(user.ID, 3, 3)
	require.NoError(t, err)
	assert.Len(t, notifications, 2) // Only 2 remaining
}

func TestNotificationRepository_MarkAsRead(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	notification := &models.Notification{
		UserID:           user.ID,
		NotificationType: models.NotificationTaskCompleted,
		Title:            "Task Completed",
		Body:             "Your task was completed",
		Read:             false,
	}
	err := db.Create(notification).Error
	require.NoError(t, err)

	err = repo.MarkAsRead(notification.ID)
	require.NoError(t, err)

	found, err := repo.FindByID(notification.ID)
	require.NoError(t, err)
	assert.True(t, found.Read)
	assert.NotNil(t, found.ReadAt)
}

func TestNotificationRepository_MarkAllAsRead(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create multiple unread notifications
	for i := 0; i < 3; i++ {
		n := &models.Notification{
			UserID:           user.ID,
			NotificationType: models.NotificationTaskDueSoon,
			Title:            "Unread",
			Body:             "Body",
			Read:             false,
		}
		err := db.Create(n).Error
		require.NoError(t, err)
	}

	err := repo.MarkAllAsRead(user.ID)
	require.NoError(t, err)

	// Verify all are read
	count, err := repo.CountUnread(user.ID)
	require.NoError(t, err)
	assert.Equal(t, int64(0), count)
}

func TestNotificationRepository_CountUnread(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create 3 unread and 2 read notifications
	for i := 0; i < 3; i++ {
		n := &models.Notification{
			UserID:           user.ID,
			NotificationType: models.NotificationTaskDueSoon,
			Title:            "Unread",
			Body:             "Body",
			Read:             false,
		}
		err := db.Create(n).Error
		require.NoError(t, err)
	}
	for i := 0; i < 2; i++ {
		n := &models.Notification{
			UserID:           user.ID,
			NotificationType: models.NotificationTaskCompleted,
			Title:            "Read",
			Body:             "Body",
			Read:             true,
		}
		err := db.Create(n).Error
		require.NoError(t, err)
	}

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

func TestNotificationRepository_MarkAsSent(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	notification := &models.Notification{
		UserID:           user.ID,
		NotificationType: models.NotificationTaskDueSoon,
		Title:            "To Send",
		Body:             "Body",
		Sent:             false,
	}
	err := db.Create(notification).Error
	require.NoError(t, err)

	err = repo.MarkAsSent(notification.ID)
	require.NoError(t, err)

	found, err := repo.FindByID(notification.ID)
	require.NoError(t, err)
	assert.True(t, found.Sent)
	assert.NotNil(t, found.SentAt)
}

func TestNotificationRepository_SetError(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	notification := &models.Notification{
		UserID:           user.ID,
		NotificationType: models.NotificationTaskDueSoon,
		Title:            "Error Notification",
		Body:             "Body",
	}
	err := db.Create(notification).Error
	require.NoError(t, err)

	err = repo.SetError(notification.ID, "failed to send: connection timeout")
	require.NoError(t, err)

	found, err := repo.FindByID(notification.ID)
	require.NoError(t, err)
	assert.Equal(t, "failed to send: connection timeout", found.ErrorMessage)
}

func TestNotificationRepository_GetPendingNotifications(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create pending (unsent) notifications
	for i := 0; i < 5; i++ {
		n := &models.Notification{
			UserID:           user.ID,
			NotificationType: models.NotificationTaskDueSoon,
			Title:            "Pending",
			Body:             "Body",
			Sent:             false,
		}
		err := db.Create(n).Error
		require.NoError(t, err)
	}

	// Create sent notification
	sent := &models.Notification{
		UserID:           user.ID,
		NotificationType: models.NotificationTaskCompleted,
		Title:            "Already Sent",
		Body:             "Body",
		Sent:             true,
	}
	err := db.Create(sent).Error
	require.NoError(t, err)

	pending, err := repo.GetPendingNotifications(10)
	require.NoError(t, err)
	assert.Len(t, pending, 5) // Only unsent

	// Test with limit
	pending, err = repo.GetPendingNotifications(3)
	require.NoError(t, err)
	assert.Len(t, pending, 3)
}

func TestNotificationRepository_APNSDevice_CRUD(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create APNS device
	device := &models.APNSDevice{
		Name:           "iPhone 15",
		Active:         true,
		UserID:         &user.ID,
		DeviceID:       "device-uuid-123",
		RegistrationID: "apns-token-abc123",
	}
	err := repo.CreateAPNSDevice(device)
	require.NoError(t, err)
	assert.NotZero(t, device.ID)

	// Find by ID
	found, err := repo.FindAPNSDeviceByID(device.ID)
	require.NoError(t, err)
	assert.Equal(t, "iPhone 15", found.Name)
	assert.Equal(t, "apns-token-abc123", found.RegistrationID)

	// Find by token
	foundByToken, err := repo.FindAPNSDeviceByToken("apns-token-abc123")
	require.NoError(t, err)
	assert.Equal(t, device.ID, foundByToken.ID)

	// Find by user
	devices, err := repo.FindAPNSDevicesByUser(user.ID)
	require.NoError(t, err)
	assert.Len(t, devices, 1)

	// Update
	device.Name = "iPhone 16"
	err = repo.UpdateAPNSDevice(device)
	require.NoError(t, err)

	found, err = repo.FindAPNSDeviceByID(device.ID)
	require.NoError(t, err)
	assert.Equal(t, "iPhone 16", found.Name)

	// Deactivate
	err = repo.DeactivateAPNSDevice(device.ID)
	require.NoError(t, err)

	// Should not appear in active devices
	devices, err = repo.FindAPNSDevicesByUser(user.ID)
	require.NoError(t, err)
	assert.Len(t, devices, 0)

	// Delete
	err = repo.DeleteAPNSDevice(device.ID)
	require.NoError(t, err)

	_, err = repo.FindAPNSDeviceByID(device.ID)
	assert.Error(t, err)
}

func TestNotificationRepository_GCMDevice_CRUD(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create GCM device
	device := &models.GCMDevice{
		Name:             "Pixel 8",
		Active:           true,
		UserID:           &user.ID,
		DeviceID:         "android-device-uuid",
		RegistrationID:   "fcm-token-xyz789",
		CloudMessageType: "FCM",
	}
	err := repo.CreateGCMDevice(device)
	require.NoError(t, err)
	assert.NotZero(t, device.ID)

	// Find by ID
	found, err := repo.FindGCMDeviceByID(device.ID)
	require.NoError(t, err)
	assert.Equal(t, "Pixel 8", found.Name)
	assert.Equal(t, "fcm-token-xyz789", found.RegistrationID)

	// Find by token
	foundByToken, err := repo.FindGCMDeviceByToken("fcm-token-xyz789")
	require.NoError(t, err)
	assert.Equal(t, device.ID, foundByToken.ID)

	// Find by user
	devices, err := repo.FindGCMDevicesByUser(user.ID)
	require.NoError(t, err)
	assert.Len(t, devices, 1)

	// Update
	device.Name = "Pixel 9"
	err = repo.UpdateGCMDevice(device)
	require.NoError(t, err)

	found, err = repo.FindGCMDeviceByID(device.ID)
	require.NoError(t, err)
	assert.Equal(t, "Pixel 9", found.Name)

	// Deactivate
	err = repo.DeactivateGCMDevice(device.ID)
	require.NoError(t, err)

	devices, err = repo.FindGCMDevicesByUser(user.ID)
	require.NoError(t, err)
	assert.Len(t, devices, 0)

	// Delete
	err = repo.DeleteGCMDevice(device.ID)
	require.NoError(t, err)

	_, err = repo.FindGCMDeviceByID(device.ID)
	assert.Error(t, err)
}

func TestNotificationRepository_GetActiveTokensForUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create APNS devices
	apns1 := &models.APNSDevice{
		Active:         true,
		UserID:         &user.ID,
		RegistrationID: "ios-token-1",
	}
	err := db.Create(apns1).Error
	require.NoError(t, err)

	apns2 := &models.APNSDevice{
		Active:         true,
		UserID:         &user.ID,
		RegistrationID: "ios-token-2",
	}
	err = db.Create(apns2).Error
	require.NoError(t, err)

	// Create inactive APNS device (should not be returned)
	// Insert inactive device via raw SQL to bypass GORM's default:true override
	err = db.Exec("INSERT INTO push_notifications_apnsdevice (active, user_id, registration_id, date_created) VALUES (false, ?, 'ios-token-inactive', CURRENT_TIMESTAMP)", user.ID).Error
	require.NoError(t, err)

	// Create GCM device
	gcm1 := &models.GCMDevice{
		Active:           true,
		UserID:           &user.ID,
		RegistrationID:   "android-token-1",
		CloudMessageType: "FCM",
	}
	err = db.Create(gcm1).Error
	require.NoError(t, err)

	iosTokens, androidTokens, err := repo.GetActiveTokensForUser(user.ID)
	require.NoError(t, err)
	assert.Len(t, iosTokens, 2)
	assert.Contains(t, iosTokens, "ios-token-1")
	assert.Contains(t, iosTokens, "ios-token-2")
	assert.Len(t, androidTokens, 1)
	assert.Contains(t, androidTokens, "android-token-1")
}

func TestNotificationRepository_GetActiveTokensForUser_NoDevices(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	iosTokens, androidTokens, err := repo.GetActiveTokensForUser(user.ID)
	require.NoError(t, err)
	assert.Empty(t, iosTokens)
	assert.Empty(t, androidTokens)
}

func TestNotificationRepository_FindPreferencesByUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	// Create preferences with defaults (GORM applies default:true for bools)
	prefs := &models.NotificationPreference{
		UserID: user.ID,
	}
	err := db.Create(prefs).Error
	require.NoError(t, err)

	// Update one field to false via raw SQL to bypass GORM default
	err = db.Exec("UPDATE notifications_notificationpreference SET task_due_soon = false WHERE user_id = ?", user.ID).Error
	require.NoError(t, err)

	found, err := repo.FindPreferencesByUser(user.ID)
	require.NoError(t, err)
	assert.Equal(t, user.ID, found.UserID)
	assert.False(t, found.TaskDueSoon)
	assert.True(t, found.TaskOverdue)
}

func TestNotificationRepository_FindPreferencesByUser_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

	_, err := repo.FindPreferencesByUser(9999)
	assert.Error(t, err)
}

func TestNotificationRepository_UpdatePreferences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewNotificationRepository(db)

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

	prefs, err := repo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)

	prefs.TaskDueSoon = false
	prefs.DailyDigest = false
	err = repo.UpdatePreferences(prefs)
	require.NoError(t, err)

	found, err := repo.FindPreferencesByUser(user.ID)
	require.NoError(t, err)
	assert.False(t, found.TaskDueSoon)
	assert.False(t, found.DailyDigest)
}
