package notifications import ( "fmt" "math/rand" "slices" "testing" "time" ) // TestReminderScheduleWith100RandomTasks creates 100 tasks with random frequencies // and validates that notifications are sent on exactly the right days according to // the current FrequencySchedules and OverdueConfig. // // This test is dynamic - it reads from the config, so if the config changes, // the test will still validate the correct behavior. func TestReminderScheduleWith100RandomTasks(t *testing.T) { // Seed random for reproducibility in tests (use current time for variety) rng := rand.New(rand.NewSource(time.Now().UnixNano())) // Available frequency intervals (from OrderedFrequencies) // We'll also add some custom intervals to test the tier mapping frequencyIntervals := []int{0, 1, 7, 14, 30, 90, 180, 365} customIntervals := []int{5, 10, 20, 45, 120, 200, 400} // Custom intervals to test tier mapping allIntervals := append(frequencyIntervals, customIntervals...) // Base date for tests - use a fixed date for deterministic behavior baseDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) // Track test statistics tasksCreated := 0 daysSimulated := 0 notificationsSent := 0 notificationsExpected := 0 for i := 0; i < 100; i++ { // Random frequency intervalIdx := rng.Intn(len(allIntervals)) intervalDays := allIntervals[intervalIdx] // Random due date offset (-30 to +30 days from base date) dueDateOffset := rng.Intn(61) - 30 dueDate := baseDate.AddDate(0, 0, dueDateOffset) // Create frequency pointer (nil for "Once" which is interval 0) var frequencyDays *int if intervalDays > 0 { frequencyDays = &intervalDays } tasksCreated++ // Get the expected schedule for this frequency expectedSchedule := GetScheduleForFrequency(frequencyDays) expectedOverdueDays := GetOverdueReminderDays() // Simulate each day from 45 days before to 15 days after due date for dayOffset := -45; dayOffset <= 15; dayOffset++ { today := dueDate.AddDate(0, 0, -dayOffset) // dayOffset is days until due, so we subtract daysSimulated++ // Get the reminder stage for today stage := GetReminderStageForToday(dueDate, frequencyDays, today) // Calculate what we expect expectedStage := calculateExpectedStage(dayOffset, expectedSchedule, expectedOverdueDays) if stage != expectedStage { t.Errorf("Task %d (interval=%d, dueDate=%s): on %s (dayOffset=%d), got stage=%q, expected=%q", i+1, intervalDays, dueDate.Format("2006-01-02"), today.Format("2006-01-02"), dayOffset, stage, expectedStage) } if stage != "" { notificationsSent++ } if expectedStage != "" { notificationsExpected++ } } } t.Logf("Test Summary:") t.Logf(" Tasks created: %d", tasksCreated) t.Logf(" Days simulated: %d", daysSimulated) t.Logf(" Notifications sent: %d", notificationsSent) t.Logf(" Notifications expected: %d", notificationsExpected) if notificationsSent != notificationsExpected { t.Errorf("Notification count mismatch: sent=%d, expected=%d", notificationsSent, notificationsExpected) } } // calculateExpectedStage determines what stage should be returned based on the config func calculateExpectedStage(dayOffset int, preReminderDays []int, overdueDays []int) string { if dayOffset > 0 { // Task is in the future (dayOffset days until due) if slices.Contains(preReminderDays, dayOffset) { return formatDaysBeforeStage(dayOffset) } return "" } else if dayOffset == 0 { // Task is due today return "day_of" } else { // Task is overdue (-dayOffset days overdue) daysOverdue := -dayOffset if slices.Contains(overdueDays, daysOverdue) { return formatOverdueStage(daysOverdue) } return "" } } // TestEachFrequencySchedule validates each frequency's schedule explicitly func TestEachFrequencySchedule(t *testing.T) { dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) // Test each configured frequency for intervalDays, expectedSchedule := range FrequencySchedules { t.Run(fmt.Sprintf("Interval_%d_days", intervalDays), func(t *testing.T) { var frequencyDays *int if intervalDays > 0 { frequencyDays = &intervalDays } // Verify all expected reminder days trigger notifications for _, reminderDay := range expectedSchedule { today := dueDate.AddDate(0, 0, -reminderDay) stage := GetReminderStageForToday(dueDate, frequencyDays, today) expectedStage := formatDaysBeforeStage(reminderDay) if stage != expectedStage { t.Errorf("Reminder %d days before: got %q, expected %q", reminderDay, stage, expectedStage) } } // Verify days NOT in the schedule don't trigger notifications (before due date) for day := 1; day <= 45; day++ { if !slices.Contains(expectedSchedule, day) { today := dueDate.AddDate(0, 0, -day) stage := GetReminderStageForToday(dueDate, frequencyDays, today) if stage != "" { t.Errorf("Day %d before due should NOT send notification for interval %d, but got %q", day, intervalDays, stage) } } } }) } } // TestOverdueReminderSchedule validates the overdue tapering logic func TestOverdueReminderSchedule(t *testing.T) { dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) // Use nil frequency (Once) - overdue logic is the same for all frequencies var frequencyDays *int // Get expected overdue days from config expectedOverdueDays := GetOverdueReminderDays() t.Logf("OverdueConfig: DailyReminderDays=%d, TaperIntervalDays=%d, MaxOverdueDays=%d", OverdueConfig.DailyReminderDays, OverdueConfig.TaperIntervalDays, OverdueConfig.MaxOverdueDays) t.Logf("Expected overdue reminder days: %v", expectedOverdueDays) // Test each day from 1 to MaxOverdueDays + 5 for daysOverdue := 1; daysOverdue <= OverdueConfig.MaxOverdueDays+5; daysOverdue++ { today := dueDate.AddDate(0, 0, daysOverdue) stage := GetReminderStageForToday(dueDate, frequencyDays, today) shouldSend := slices.Contains(expectedOverdueDays, daysOverdue) expectedStage := "" if shouldSend { expectedStage = formatOverdueStage(daysOverdue) } if stage != expectedStage { t.Errorf("Day %d overdue: got %q, expected %q (shouldSend=%v)", daysOverdue, stage, expectedStage, shouldSend) } } } // TestCustomIntervalTierMapping validates that custom intervals map to correct tiers func TestCustomIntervalTierMapping(t *testing.T) { testCases := []struct { customInterval int expectedTier int expectedMessage string }{ {2, 7, "2 days should map to Weekly (7) tier"}, {5, 7, "5 days should map to Weekly (7) tier"}, {8, 14, "8 days should map to Bi-Weekly (14) tier"}, {10, 14, "10 days should map to Bi-Weekly (14) tier"}, {20, 30, "20 days should map to Monthly (30) tier"}, {45, 90, "45 days should map to Quarterly (90) tier"}, {100, 180, "100 days should map to Semi-Annually (180) tier"}, {200, 365, "200 days should map to Annually (365) tier"}, {500, 365, "500 days should map to Annually (365) tier - fallback"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("CustomInterval_%d", tc.customInterval), func(t *testing.T) { schedule := GetScheduleForCustomInterval(tc.customInterval) expectedSchedule := FrequencySchedules[tc.expectedTier] if !slices.Equal(schedule, expectedSchedule) { t.Errorf("%s: got schedule %v, expected %v (tier %d)", tc.expectedMessage, schedule, expectedSchedule, tc.expectedTier) } }) } } // TestDayOfNotification ensures day-of notifications always fire func TestDayOfNotification(t *testing.T) { dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) today := dueDate // Same day // Test all frequency types - day-of should always fire for intervalDays := range FrequencySchedules { t.Run(fmt.Sprintf("DayOf_Interval_%d", intervalDays), func(t *testing.T) { var frequencyDays *int if intervalDays > 0 { frequencyDays = &intervalDays } stage := GetReminderStageForToday(dueDate, frequencyDays, today) if stage != "day_of" { t.Errorf("Day-of notification for interval %d: got %q, expected \"day_of\"", intervalDays, stage) } }) } } // TestNoNotificationAfterMaxOverdue ensures no notifications after MaxOverdueDays func TestNoNotificationAfterMaxOverdue(t *testing.T) { dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) var frequencyDays *int // Once frequency // Test days beyond MaxOverdueDays for daysOverdue := OverdueConfig.MaxOverdueDays + 1; daysOverdue <= OverdueConfig.MaxOverdueDays+30; daysOverdue++ { today := dueDate.AddDate(0, 0, daysOverdue) stage := GetReminderStageForToday(dueDate, frequencyDays, today) if stage != "" { t.Errorf("Day %d overdue (beyond MaxOverdueDays=%d): should not send notification, but got %q", daysOverdue, OverdueConfig.MaxOverdueDays, stage) } } } // TestNoNotificationTooFarInFuture ensures no notifications for tasks far in the future func TestNoNotificationTooFarInFuture(t *testing.T) { dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) // Get the maximum pre-reminder days from all schedules maxPreReminderDays := 0 for _, schedule := range FrequencySchedules { for _, days := range schedule { if days > maxPreReminderDays { maxPreReminderDays = days } } } // Test for each frequency type - days beyond their schedule should not trigger for intervalDays, schedule := range FrequencySchedules { t.Run(fmt.Sprintf("FarFuture_Interval_%d", intervalDays), func(t *testing.T) { var frequencyDays *int if intervalDays > 0 { frequencyDays = &intervalDays } // Find the max reminder day for this frequency maxForFrequency := 0 for _, d := range schedule { if d > maxForFrequency { maxForFrequency = d } } // Test days beyond the max reminder day for this frequency for daysBefore := maxForFrequency + 1; daysBefore <= maxPreReminderDays+10; daysBefore++ { // Skip if this day is actually in the schedule if slices.Contains(schedule, daysBefore) { continue } today := dueDate.AddDate(0, 0, -daysBefore) stage := GetReminderStageForToday(dueDate, frequencyDays, today) if stage != "" { t.Errorf("Day %d before due (beyond schedule max=%d for interval %d): should not send, but got %q", daysBefore, maxForFrequency, intervalDays, stage) } } }) } } // TestConfigConsistency validates that the configuration is internally consistent func TestConfigConsistency(t *testing.T) { // Verify GetOverdueReminderDays matches ShouldSendOverdueReminder overdueDays := GetOverdueReminderDays() for day := 1; day <= OverdueConfig.MaxOverdueDays+5; day++ { shouldSend := ShouldSendOverdueReminder(day) isInList := slices.Contains(overdueDays, day) if shouldSend != isInList { t.Errorf("Inconsistency at day %d: ShouldSendOverdueReminder=%v, but day in GetOverdueReminderDays=%v", day, shouldSend, isInList) } } // Verify all frequencies have 0 (day-of) in their schedule for intervalDays, schedule := range FrequencySchedules { if !slices.Contains(schedule, 0) { t.Errorf("Frequency %d days is missing day-of (0) in schedule: %v", intervalDays, schedule) } } // Verify schedules are sorted in descending order (most days first) for intervalDays, schedule := range FrequencySchedules { for i := 0; i < len(schedule)-1; i++ { if schedule[i] < schedule[i+1] { t.Errorf("Frequency %d schedule is not in descending order: %v", intervalDays, schedule) break } } } } // TestStageFormatting validates the stage string formatting func TestStageFormatting(t *testing.T) { testCases := []struct { daysBefore int expectedStage string }{ {0, "day_of"}, {1, "reminder_1d"}, {3, "reminder_3d"}, {7, "reminder_7d"}, {14, "reminder_14d"}, {30, "reminder_30d"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("Format_%d_days_before", tc.daysBefore), func(t *testing.T) { stage := formatDaysBeforeStage(tc.daysBefore) if stage != tc.expectedStage { t.Errorf("formatDaysBeforeStage(%d) = %q, expected %q", tc.daysBefore, stage, tc.expectedStage) } }) } // Test overdue formatting overdueTestCases := []struct { daysOverdue int expectedStage string }{ {1, "overdue_1"}, {3, "overdue_3"}, {7, "overdue_7"}, {14, "overdue_14"}, } for _, tc := range overdueTestCases { t.Run(fmt.Sprintf("Format_%d_days_overdue", tc.daysOverdue), func(t *testing.T) { stage := formatOverdueStage(tc.daysOverdue) if stage != tc.expectedStage { t.Errorf("formatOverdueStage(%d) = %q, expected %q", tc.daysOverdue, stage, tc.expectedStage) } }) } } // BenchmarkGetReminderStageForToday benchmarks the main function func BenchmarkGetReminderStageForToday(b *testing.B) { dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) today := time.Date(2025, 6, 12, 0, 0, 0, 0, time.UTC) // 3 days before intervalDays := 30 frequencyDays := &intervalDays b.ResetTimer() for i := 0; i < b.N; i++ { GetReminderStageForToday(dueDate, frequencyDays, today) } } // TestReminderScheduleTableDriven uses the config to generate a comprehensive // table-driven test that validates all expected notification days func TestReminderScheduleTableDriven(t *testing.T) { dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) // Build test cases from config for intervalDays, preSchedule := range FrequencySchedules { var frequencyDays *int if intervalDays > 0 { frequencyDays = &intervalDays } freqName := FrequencyNames[intervalDays] if freqName == "" { freqName = fmt.Sprintf("Custom_%d", intervalDays) } // Test pre-due reminders for _, daysBefore := range preSchedule { t.Run(fmt.Sprintf("%s_PreReminder_%dd", freqName, daysBefore), func(t *testing.T) { today := dueDate.AddDate(0, 0, -daysBefore) stage := GetReminderStageForToday(dueDate, frequencyDays, today) expected := formatDaysBeforeStage(daysBefore) if stage != expected { t.Errorf("got %q, expected %q", stage, expected) } }) } // Test overdue reminders (same for all frequencies) overdueDays := GetOverdueReminderDays() for _, daysOverdue := range overdueDays { t.Run(fmt.Sprintf("%s_Overdue_%dd", freqName, daysOverdue), func(t *testing.T) { today := dueDate.AddDate(0, 0, daysOverdue) stage := GetReminderStageForToday(dueDate, frequencyDays, today) expected := formatOverdueStage(daysOverdue) if stage != expected { t.Errorf("got %q, expected %q", stage, expected) } }) } } } // TestFullSimulation100Tasks is the main comprehensive test that simulates // 100 random tasks over the full date range func TestFullSimulation100Tasks(t *testing.T) { // Use a fixed seed for reproducible tests rng := rand.New(rand.NewSource(42)) // All available frequencies including custom frequencies := []int{0, 1, 7, 14, 30, 90, 180, 365, 5, 10, 20, 45, 100, 200, 500} baseDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC) // Statistics totalDays := 0 totalNotifications := 0 notificationsByStage := make(map[string]int) for taskIdx := 0; taskIdx < 100; taskIdx++ { // Random frequency intervalDays := frequencies[rng.Intn(len(frequencies))] // Random due date (within 60-day range around base) dueDateOffset := rng.Intn(61) - 30 dueDate := baseDate.AddDate(0, 0, dueDateOffset) var frequencyDays *int if intervalDays > 0 { frequencyDays = &intervalDays } // Get expected schedules from config preSchedule := GetScheduleForFrequency(frequencyDays) overdueDays := GetOverdueReminderDays() // Simulate 45 days before to 15 days after due date for dayOffset := -45; dayOffset <= 15; dayOffset++ { // dayOffset: negative = future (before due), positive = past (overdue) // So if dayOffset = -10, we're 10 days BEFORE due date // If dayOffset = 5, we're 5 days AFTER due date (overdue) today := dueDate.AddDate(0, 0, dayOffset) totalDays++ stage := GetReminderStageForToday(dueDate, frequencyDays, today) // Calculate expected stage var expectedStage string if dayOffset < 0 { // Before due date: check pre-reminders daysBefore := -dayOffset if slices.Contains(preSchedule, daysBefore) { expectedStage = formatDaysBeforeStage(daysBefore) } } else if dayOffset == 0 { // Day of expectedStage = "day_of" } else { // Overdue if slices.Contains(overdueDays, dayOffset) { expectedStage = formatOverdueStage(dayOffset) } } if stage != expectedStage { t.Errorf("Task %d (freq=%d, due=%s): day %s (offset=%d): got %q, expected %q", taskIdx+1, intervalDays, dueDate.Format("2006-01-02"), today.Format("2006-01-02"), dayOffset, stage, expectedStage) } if stage != "" { totalNotifications++ notificationsByStage[stage]++ } } } t.Logf("Full Simulation Results:") t.Logf(" Total tasks: 100") t.Logf(" Total days simulated: %d", totalDays) t.Logf(" Total notifications: %d", totalNotifications) t.Logf(" Notifications by stage:") for stage, count := range notificationsByStage { t.Logf(" %s: %d", stage, count) } }