package services

import (
	"context"
	"net/http"
	"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/push"
	"github.com/treytartt/honeydue-api/internal/repositories"
	"github.com/treytartt/honeydue-api/internal/testutil"
)

func setupNotificationService(t *testing.T) (*NotificationService, *repositories.NotificationRepository) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	// pushClient is nil for testing (no actual push sends)
	service := NewNotificationService(notifRepo, nil)
	return service, notifRepo
}

// === GetNotifications ===

func TestNotificationService_GetNotifications(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Create some notifications
	for i := 0; i < 3; i++ {
		notif := &models.Notification{
			UserID:           user.ID,
			NotificationType: models.NotificationTaskDueSoon,
			Title:            "Test Notification",
			Body:             "Some task is due soon",
		}
		err := db.Create(notif).Error
		require.NoError(t, err)
	}

	resp, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, resp, 3)
}

func TestNotificationService_GetNotifications_Empty(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	resp, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Empty(t, resp)
}

func TestNotificationService_GetNotifications_Pagination(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

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

	// Get first 2
	resp, err := service.GetNotifications(user.ID, 2, 0)
	require.NoError(t, err)
	assert.Len(t, resp, 2)
}

// === GetUnreadCount ===

func TestNotificationService_GetUnreadCount(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

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

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

func TestNotificationService_GetUnreadCount_Zero(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	count, err := service.GetUnreadCount(user.ID)
	require.NoError(t, err)
	assert.Equal(t, int64(0), count)
}

// === MarkAsRead ===

func TestNotificationService_MarkAsRead(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

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

	err = service.MarkAsRead(notif.ID, user.ID)
	require.NoError(t, err)

	// Verify unread count is 0
	count, err := service.GetUnreadCount(user.ID)
	require.NoError(t, err)
	assert.Equal(t, int64(0), count)
}

func TestNotificationService_MarkAsRead_WrongUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

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

	err = service.MarkAsRead(notif.ID, other.ID)
	testutil.AssertAppError(t, err, http.StatusNotFound, "error.notification_not_found")
}

func TestNotificationService_MarkAsRead_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.MarkAsRead(9999, user.ID)
	testutil.AssertAppError(t, err, http.StatusNotFound, "error.notification_not_found")
}

// === MarkAllAsRead ===

func TestNotificationService_MarkAllAsRead(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

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

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

	count, err := service.GetUnreadCount(user.ID)
	require.NoError(t, err)
	assert.Equal(t, int64(0), count)
}

// === CreateAndSendNotification ===

func TestNotificationService_CreateAndSendNotification(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil) // nil push client = no actual push

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

	data := map[string]interface{}{
		"task_id": 123,
	}

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskDueSoon, "Due Soon", "Fix faucet", data)
	require.NoError(t, err)

	// Verify notification was created
	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
	assert.Equal(t, "Due Soon", notifs[0].Title)
	assert.Equal(t, "Fix faucet", notifs[0].Body)
}

func TestNotificationService_CreateAndSendNotification_DisabledPreference(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Create preferences with task_due_soon disabled
	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	prefs.TaskDueSoon = false
	err = notifRepo.UpdatePreferences(prefs)
	require.NoError(t, err)

	err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskDueSoon, "Due Soon", "Fix faucet", nil)
	require.NoError(t, err)

	// Verify no notification was created (silently skipped)
	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Empty(t, notifs)
}

// === Preferences ===

func TestNotificationService_GetPreferences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	resp, err := service.GetPreferences(user.ID)
	require.NoError(t, err)
	assert.NotNil(t, resp)
	// Defaults should all be true
	assert.True(t, resp.TaskDueSoon)
	assert.True(t, resp.TaskOverdue)
	assert.True(t, resp.TaskCompleted)
}

func TestNotificationService_UpdatePreferences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	falseVal := false
	req := &UpdatePreferencesRequest{
		TaskDueSoon: &falseVal,
	}

	resp, err := service.UpdatePreferences(user.ID, req)
	require.NoError(t, err)
	assert.False(t, resp.TaskDueSoon)
	assert.True(t, resp.TaskOverdue) // unchanged
}

func TestNotificationService_UpdatePreferences_InvalidHour(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	invalidHour := 25
	req := &UpdatePreferencesRequest{
		TaskDueSoonHour: &invalidHour,
	}

	_, err := service.UpdatePreferences(user.ID, req)
	testutil.AssertAppErrorCode(t, err, http.StatusBadRequest)
}

func TestNotificationService_UpdatePreferences_ValidHour(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	hour := 9
	req := &UpdatePreferencesRequest{
		TaskDueSoonHour: &hour,
	}

	resp, err := service.UpdatePreferences(user.ID, req)
	require.NoError(t, err)
	assert.Equal(t, 9, *resp.TaskDueSoonHour)
}

// === RegisterDevice ===

func TestNotificationService_RegisterDevice_iOS(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	req := &RegisterDeviceRequest{
		Name:           "iPhone 15",
		DeviceID:       "device-abc",
		RegistrationID: "token-xyz",
		Platform:       push.PlatformIOS,
	}

	resp, err := service.RegisterDevice(user.ID, req)
	require.NoError(t, err)
	assert.Equal(t, "iPhone 15", resp.Name)
	assert.Equal(t, push.PlatformIOS, resp.Platform)
	assert.True(t, resp.Active)
}

func TestNotificationService_RegisterDevice_Android(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	req := &RegisterDeviceRequest{
		Name:           "Pixel 8",
		DeviceID:       "device-def",
		RegistrationID: "token-abc",
		Platform:       push.PlatformAndroid,
	}

	resp, err := service.RegisterDevice(user.ID, req)
	require.NoError(t, err)
	assert.Equal(t, "Pixel 8", resp.Name)
	assert.Equal(t, push.PlatformAndroid, resp.Platform)
	assert.True(t, resp.Active)
}

func TestNotificationService_RegisterDevice_InvalidPlatform(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	req := &RegisterDeviceRequest{
		Name:           "Unknown",
		DeviceID:       "device-bad",
		RegistrationID: "token-bad",
		Platform:       "windows",
	}

	_, err := service.RegisterDevice(user.ID, req)
	testutil.AssertAppError(t, err, http.StatusBadRequest, "error.invalid_platform")
}

func TestNotificationService_RegisterDevice_UpdateExisting(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Register a device
	req := &RegisterDeviceRequest{
		Name:           "iPhone 15",
		DeviceID:       "device-abc",
		RegistrationID: "token-xyz",
		Platform:       push.PlatformIOS,
	}
	_, err := service.RegisterDevice(user.ID, req)
	require.NoError(t, err)

	// Re-register with same token (should update, not duplicate)
	req.Name = "iPhone 15 Pro"
	resp, err := service.RegisterDevice(user.ID, req)
	require.NoError(t, err)
	assert.Equal(t, "iPhone 15 Pro", resp.Name)
}

// === ListDevices ===

func TestNotificationService_ListDevices(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Register iOS and Android devices
	iosDevice := &models.APNSDevice{
		UserID:         &user.ID,
		Name:           "iPhone",
		DeviceID:       "d1",
		RegistrationID: "t1",
		Active:         true,
	}
	err := db.Create(iosDevice).Error
	require.NoError(t, err)

	androidDevice := &models.GCMDevice{
		UserID:           &user.ID,
		Name:             "Pixel",
		DeviceID:         "d2",
		RegistrationID:   "t2",
		CloudMessageType: "FCM",
		Active:           true,
	}
	err = db.Create(androidDevice).Error
	require.NoError(t, err)

	resp, err := service.ListDevices(user.ID)
	require.NoError(t, err)
	assert.Len(t, resp, 2)
}

func TestNotificationService_ListDevices_Empty(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	resp, err := service.ListDevices(user.ID)
	require.NoError(t, err)
	assert.Empty(t, resp)
}

// === DeleteDevice - Invalid Platform ===

func TestDeleteDevice_InvalidPlatform_Returns400(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.DeleteDevice(1, "windows", user.ID)
	testutil.AssertAppError(t, err, http.StatusBadRequest, "error.invalid_platform")
}

// === UnregisterDevice ===

func TestNotificationService_UnregisterDevice_iOS(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	device := &models.APNSDevice{
		UserID:         &user.ID,
		Name:           "iPhone",
		DeviceID:       "d1",
		RegistrationID: "reg-token-ios",
		Active:         true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	err = service.UnregisterDevice("reg-token-ios", push.PlatformIOS, user.ID)
	require.NoError(t, err)

	// Verify device is deactivated
	var found models.APNSDevice
	err = db.First(&found, device.ID).Error
	require.NoError(t, err)
	assert.False(t, found.Active)
}

func TestNotificationService_UnregisterDevice_Android(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	device := &models.GCMDevice{
		UserID:           &user.ID,
		Name:             "Pixel",
		DeviceID:         "d2",
		RegistrationID:   "reg-token-android",
		CloudMessageType: "FCM",
		Active:           true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	err = service.UnregisterDevice("reg-token-android", push.PlatformAndroid, user.ID)
	require.NoError(t, err)

	var found models.GCMDevice
	err = db.First(&found, device.ID).Error
	require.NoError(t, err)
	assert.False(t, found.Active)
}

func TestNotificationService_UnregisterDevice_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.UnregisterDevice("nonexistent-token", push.PlatformIOS, user.ID)
	testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
}

func TestNotificationService_UnregisterDevice_WrongUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	device := &models.APNSDevice{
		UserID:         &owner.ID,
		Name:           "iPhone",
		DeviceID:       "d1",
		RegistrationID: "owner-token",
		Active:         true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	err = service.UnregisterDevice("owner-token", push.PlatformIOS, attacker.ID)
	testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
}

func TestNotificationService_UnregisterDevice_InvalidPlatform(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.UnregisterDevice("some-token", "windows", user.ID)
	testutil.AssertAppError(t, err, http.StatusBadRequest, "error.invalid_platform")
}

// === UpdateUserTimezone ===

func TestNotificationService_UpdateUserTimezone(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Should not panic, just silently update
	service.UpdateUserTimezone(user.ID, "America/Los_Angeles")

	// Verify timezone was stored
	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	require.NotNil(t, prefs.Timezone)
	assert.Equal(t, "America/Los_Angeles", *prefs.Timezone)
}

func TestNotificationService_UpdateUserTimezone_Invalid(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Invalid timezone should be silently ignored
	service.UpdateUserTimezone(user.ID, "Invalid/Timezone")

	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	assert.Nil(t, prefs.Timezone) // Should not have been set
}

func TestNotificationService_UpdateUserTimezone_NoChangeSkipsWrite(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Set timezone
	service.UpdateUserTimezone(user.ID, "America/New_York")

	// Set same timezone again — should be a no-op
	service.UpdateUserTimezone(user.ID, "America/New_York")

	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	require.NotNil(t, prefs.Timezone)
	assert.Equal(t, "America/New_York", *prefs.Timezone)
}

func TestDeleteDevice_WrongUser_Returns403(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	attacker := testutil.CreateTestUser(t, db, "attacker", "attacker@test.com", "password")

	// Register an iOS device for the owner
	device := &models.APNSDevice{
		UserID:         &owner.ID,
		Name:           "Owner iPhone",
		DeviceID:       "device-123",
		RegistrationID: "token-abc",
		Active:         true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	// Attacker tries to deactivate the owner's device
	err = service.DeleteDevice(device.ID, push.PlatformIOS, attacker.ID)
	require.Error(t, err, "should not allow deleting another user's device")
	testutil.AssertAppErrorCode(t, err, http.StatusForbidden)

	// Verify the device is still active
	var found models.APNSDevice
	err = db.First(&found, device.ID).Error
	require.NoError(t, err)
	assert.True(t, found.Active, "device should still be active after failed deletion")
}

func TestDeleteDevice_CorrectUser_Succeeds(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	// Register an iOS device for the owner
	device := &models.APNSDevice{
		UserID:         &owner.ID,
		Name:           "Owner iPhone",
		DeviceID:       "device-123",
		RegistrationID: "token-abc",
		Active:         true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	// Owner deactivates their own device
	err = service.DeleteDevice(device.ID, push.PlatformIOS, owner.ID)
	require.NoError(t, err, "owner should be able to deactivate their own device")

	// Verify the device is now inactive
	var found models.APNSDevice
	err = db.First(&found, device.ID).Error
	require.NoError(t, err)
	assert.False(t, found.Active, "device should be deactivated")
}

func TestDeleteDevice_WrongUser_Android_Returns403(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	attacker := testutil.CreateTestUser(t, db, "attacker", "attacker@test.com", "password")

	// Register an Android device for the owner
	device := &models.GCMDevice{
		UserID:           &owner.ID,
		Name:             "Owner Pixel",
		DeviceID:         "device-456",
		RegistrationID:   "token-def",
		CloudMessageType: "FCM",
		Active:           true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	// Attacker tries to deactivate the owner's Android device
	err = service.DeleteDevice(device.ID, push.PlatformAndroid, attacker.ID)
	require.Error(t, err, "should not allow deleting another user's Android device")
	testutil.AssertAppErrorCode(t, err, http.StatusForbidden)

	// Verify the device is still active
	var found models.GCMDevice
	err = db.First(&found, device.ID).Error
	require.NoError(t, err)
	assert.True(t, found.Active, "Android device should still be active after failed deletion")
}

func TestDeleteDevice_NonExistent_Returns404(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.DeleteDevice(99999, push.PlatformIOS, user.ID)
	require.Error(t, err, "should return error for non-existent device")
	testutil.AssertAppErrorCode(t, err, http.StatusNotFound)
}

// === CreateAndSendNotification — all notification types ===

func TestNotificationService_CreateAndSend_TaskOverdue(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskOverdue, "Overdue", "Task is overdue", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
	assert.Equal(t, "Overdue", notifs[0].Title)
}

func TestNotificationService_CreateAndSend_TaskCompleted(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskCompleted, "Completed", "Task done", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
}

func TestNotificationService_CreateAndSend_TaskAssigned(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskAssigned, "Assigned", "Task assigned to you", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
}

func TestNotificationService_CreateAndSend_ResidenceShared(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationResidenceShared, "Shared", "Someone shared a home", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
}

func TestNotificationService_CreateAndSend_WarrantyExpiring(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationWarrantyExpiring, "Expiring", "Warranty expiring soon", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
}

// === CreateAndSendNotification — disabled preference for each type ===

func TestNotificationService_DisabledPrefs_TaskOverdue(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	prefs.TaskOverdue = false
	err = notifRepo.UpdatePreferences(prefs)
	require.NoError(t, err)

	err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskOverdue, "Overdue", "Overdue task", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Empty(t, notifs)
}

func TestNotificationService_DisabledPrefs_TaskCompleted(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	prefs.TaskCompleted = false
	err = notifRepo.UpdatePreferences(prefs)
	require.NoError(t, err)

	err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskCompleted, "Completed", "Task done", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Empty(t, notifs)
}

func TestNotificationService_DisabledPrefs_TaskAssigned(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	prefs.TaskAssigned = false
	err = notifRepo.UpdatePreferences(prefs)
	require.NoError(t, err)

	err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskAssigned, "Assigned", "Task assigned", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Empty(t, notifs)
}

func TestNotificationService_DisabledPrefs_ResidenceShared(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	prefs.ResidenceShared = false
	err = notifRepo.UpdatePreferences(prefs)
	require.NoError(t, err)

	err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationResidenceShared, "Shared", "Home shared", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Empty(t, notifs)
}

func TestNotificationService_DisabledPrefs_WarrantyExpiring(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
	require.NoError(t, err)
	prefs.WarrantyExpiring = false
	err = notifRepo.UpdatePreferences(prefs)
	require.NoError(t, err)

	err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationWarrantyExpiring, "Warranty", "Expiring", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Empty(t, notifs)
}

// === CreateAndSendNotification — unknown type defaults to enabled ===

func TestNotificationService_CreateAndSend_UnknownTypeDefaultsEnabled(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationType("unknown_type"), "Unknown", "Unknown notification", nil)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
}

// === CreateAndSendNotification with string and non-string data values ===

func TestNotificationService_CreateAndSend_WithMixedDataTypes(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	data := map[string]interface{}{
		"task_id":      uint(42),
		"task_name":    "Fix faucet",
		"residence_id": uint(10),
		"extra_data":   map[string]string{"foo": "bar"},
	}

	err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskDueSoon, "Due Soon", "Fix faucet", data)
	require.NoError(t, err)

	notifs, err := service.GetNotifications(user.ID, 10, 0)
	require.NoError(t, err)
	assert.Len(t, notifs, 1)
	assert.NotNil(t, notifs[0].Data)
}

// === UpdatePreferences — partial update with multiple fields ===

func TestNotificationService_UpdatePreferences_MultipleFields(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	falseVal := false
	trueVal := true
	hour8 := 8
	hour14 := 14
	req := &UpdatePreferencesRequest{
		TaskDueSoon:          &falseVal,
		TaskOverdue:          &falseVal,
		TaskCompleted:        &trueVal,
		TaskAssigned:         &trueVal,
		ResidenceShared:      &falseVal,
		WarrantyExpiring:     &trueVal,
		DailyDigest:          &falseVal,
		EmailTaskCompleted:   &trueVal,
		TaskDueSoonHour:      &hour8,
		TaskOverdueHour:      &hour14,
	}

	resp, err := service.UpdatePreferences(user.ID, req)
	require.NoError(t, err)
	assert.False(t, resp.TaskDueSoon)
	assert.False(t, resp.TaskOverdue)
	assert.True(t, resp.TaskCompleted)
	assert.True(t, resp.TaskAssigned)
	assert.False(t, resp.ResidenceShared)
	assert.True(t, resp.WarrantyExpiring)
	assert.False(t, resp.DailyDigest)
	assert.True(t, resp.EmailTaskCompleted)
	assert.Equal(t, 8, *resp.TaskDueSoonHour)
	assert.Equal(t, 14, *resp.TaskOverdueHour)
}

// === UpdatePreferences — negative hour ===

func TestNotificationService_UpdatePreferences_NegativeHour(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	negHour := -1
	req := &UpdatePreferencesRequest{
		TaskOverdueHour: &negHour,
	}

	_, err := service.UpdatePreferences(user.ID, req)
	testutil.AssertAppErrorCode(t, err, http.StatusBadRequest)
}

// === RegisterDevice — re-register Android device with same token ===

func TestNotificationService_RegisterDevice_UpdateExistingAndroid(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	req := &RegisterDeviceRequest{
		Name:           "Pixel 8",
		DeviceID:       "device-android",
		RegistrationID: "token-android-1",
		Platform:       push.PlatformAndroid,
	}
	_, err := service.RegisterDevice(user.ID, req)
	require.NoError(t, err)

	// Re-register with same token but new name
	req.Name = "Pixel 8 Pro"
	resp, err := service.RegisterDevice(user.ID, req)
	require.NoError(t, err)
	assert.Equal(t, "Pixel 8 Pro", resp.Name)
	assert.Equal(t, push.PlatformAndroid, resp.Platform)
}

// === DeleteDevice — Android not found ===

func TestDeleteDevice_AndroidNotFound_Returns404(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.DeleteDevice(99999, push.PlatformAndroid, user.ID)
	testutil.AssertAppErrorCode(t, err, http.StatusNotFound)
}

// === DeleteDevice — Android correct user succeeds ===

func TestDeleteDevice_CorrectUser_Android_Succeeds(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	device := &models.GCMDevice{
		UserID:           &owner.ID,
		Name:             "Pixel",
		DeviceID:         "device-android-1",
		RegistrationID:   "token-android-1",
		CloudMessageType: "FCM",
		Active:           true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	err = service.DeleteDevice(device.ID, push.PlatformAndroid, owner.ID)
	require.NoError(t, err)

	var found models.GCMDevice
	err = db.First(&found, device.ID).Error
	require.NoError(t, err)
	assert.False(t, found.Active)
}

// === UnregisterDevice — Android wrong user ===

func TestNotificationService_UnregisterDevice_WrongUser_Android(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	device := &models.GCMDevice{
		UserID:           &owner.ID,
		Name:             "Pixel",
		DeviceID:         "d2",
		RegistrationID:   "owner-android-token",
		CloudMessageType: "FCM",
		Active:           true,
	}
	err := db.Create(device).Error
	require.NoError(t, err)

	err = service.UnregisterDevice("owner-android-token", push.PlatformAndroid, attacker.ID)
	testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
}

// === UnregisterDevice — Android not found ===

func TestNotificationService_UnregisterDevice_AndroidNotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	notifRepo := repositories.NewNotificationRepository(db)
	service := NewNotificationService(notifRepo, nil)

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

	err := service.UnregisterDevice("nonexistent-android", push.PlatformAndroid, user.ID)
	testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
}

// === NewNotificationResponse with data and dates ===

func TestNewNotificationResponse_WithDataAndDates(t *testing.T) {
	now := time.Now()
	readAt := now.Add(-1 * time.Hour)
	sentAt := now.Add(-2 * time.Hour)

	n := &models.Notification{
		UserID:           1,
		NotificationType: models.NotificationTaskDueSoon,
		Title:            "Test",
		Body:             "Body",
		Data:             `{"task_id": 42}`,
		Read:             true,
		ReadAt:           &readAt,
		Sent:             true,
		SentAt:           &sentAt,
	}
	n.CreatedAt = now

	resp := NewNotificationResponse(n)
	assert.Equal(t, "Test", resp.Title)
	assert.True(t, resp.Read)
	assert.True(t, resp.Sent)
	assert.NotNil(t, resp.ReadAt)
	assert.NotNil(t, resp.SentAt)
	assert.NotNil(t, resp.Data)
	assert.Equal(t, float64(42), resp.Data["task_id"])
}

func TestNewNotificationResponse_EmptyData(t *testing.T) {
	now := time.Now()
	n := &models.Notification{
		UserID:           1,
		NotificationType: models.NotificationTaskDueSoon,
		Title:            "Test",
		Body:             "Body",
		Data:             "",
	}
	n.CreatedAt = now

	resp := NewNotificationResponse(n)
	assert.Nil(t, resp.Data)
	assert.Nil(t, resp.ReadAt)
	assert.Nil(t, resp.SentAt)
}

// === validateHourField ===

func TestValidateHourField_BoundaryValues(t *testing.T) {
	zero := 0
	twentyThree := 23
	twentyFour := 24

	assert.NoError(t, validateHourField(&zero, "test"))
	assert.NoError(t, validateHourField(&twentyThree, "test"))
	assert.Error(t, validateHourField(&twentyFour, "test"))
	assert.NoError(t, validateHourField(nil, "test"))
}
