@@ -14,6 +14,8 @@ import com.github.quarck.calnotify.calendar.EventOrigin
1414import com.github.quarck.calnotify.calendar.EventRecord
1515import com.github.quarck.calnotify.calendar.EventReminderRecord
1616import com.github.quarck.calnotify.calendar.EventStatus
17+ import com.github.quarck.calnotify.calendar.displayedStartTime
18+ import com.github.quarck.calnotify.calendar.getNextAlertTimeAfter
1719import com.github.quarck.calnotify.reminders.ReminderStateInterface
1820import com.github.quarck.calnotify.utils.CNPlusUnitTestClock
1921import io.mockk.every
@@ -802,5 +804,153 @@ class EventFormatterRobolectricTest {
802804 assertEquals(" 1m" , EventFormatter .formatDurationCompact(0 ))
803805 assertEquals(" 1m" , EventFormatter .formatDurationCompact(30 * 1000L )) // 30 seconds
804806 }
807+
808+ // === Recurring event tests ===
809+
810+ @Test
811+ fun `getNextAlertTimeAfter - recurring event uses instance start time not master start time` () {
812+ // Simulate a recurring weekly meeting that started 6 months ago
813+ val sixMonthsAgo = baseTime - 180 * Consts .DAY_IN_MILLISECONDS
814+ val thisWeeksInstance = baseTime + 2 * Consts .HOUR_IN_MILLISECONDS
815+
816+ // Master event (from CalendarProvider) has old start time
817+ val masterEventRecord = createMockEventRecord(
818+ eventId = 100L ,
819+ startTime = sixMonthsAgo, // Master event started 6 months ago!
820+ reminders = listOf (EventReminderRecord .minutes(60 )) // 1 hour before
821+ )
822+
823+ // Without instance time: reminder would be 6 months ago (wrong!)
824+ val wrongResult = masterEventRecord.getNextAlertTimeAfter(baseTime)
825+ assertNull(" Without instanceStartTime, recurring event reminder would be in past" , wrongResult)
826+
827+ // With instance time: reminder should be 1 hour before this week's instance
828+ val correctResult = masterEventRecord.getNextAlertTimeAfter(baseTime, thisWeeksInstance)
829+ assertNotNull(" With instanceStartTime, should find future reminder" , correctResult)
830+
831+ val expectedReminderTime = thisWeeksInstance - Consts .HOUR_IN_MILLISECONDS
832+ assertEquals(" Reminder should be 1hr before instance start" , expectedReminderTime, correctResult)
833+ }
834+
835+ @Test
836+ fun `formatNextNotificationIndicator - recurring event shows correct indicator` () {
837+ val eventId = 500L
838+ val sixMonthsAgo = baseTime - 180 * Consts .DAY_IN_MILLISECONDS
839+ val thisWeeksInstance = baseTime + 2 * Consts .HOUR_IN_MILLISECONDS
840+
841+ // Master event has old start time (simulating recurring event)
842+ val mockCalendarProvider = mockk<CalendarProviderInterface >()
843+ every { mockCalendarProvider.getEvent(any(), eq(eventId)) } returns createMockEventRecord(
844+ eventId = eventId,
845+ startTime = sixMonthsAgo, // Master started 6 months ago
846+ reminders = listOf (EventReminderRecord .minutes(60 )) // 1 hour before
847+ )
848+
849+ val testFormatter = EventFormatter (
850+ ctx = context,
851+ clock = testClock,
852+ calendarProvider = mockCalendarProvider
853+ )
854+
855+ // Create EventAlertRecord with THIS WEEK's instance time
856+ val event = EventAlertRecord (
857+ calendarId = 1L ,
858+ eventId = eventId,
859+ isAllDay = false ,
860+ isRepeating = true , // This is a recurring event!
861+ alertTime = baseTime,
862+ notificationId = eventId.toInt(),
863+ title = " Weekly Team Meeting" ,
864+ desc = " " ,
865+ startTime = sixMonthsAgo, // Master event start (old)
866+ endTime = sixMonthsAgo + Consts .HOUR_IN_MILLISECONDS ,
867+ instanceStartTime = thisWeeksInstance, // THIS WEEK's instance
868+ instanceEndTime = thisWeeksInstance + Consts .HOUR_IN_MILLISECONDS ,
869+ location = " " ,
870+ lastStatusChangeTime = baseTime,
871+ snoozedUntil = 0L ,
872+ displayStatus = EventDisplayStatus .Hidden ,
873+ color = 0 ,
874+ origin = EventOrigin .ProviderBroadcast ,
875+ timeFirstSeen = baseTime,
876+ eventStatus = EventStatus .Confirmed ,
877+ attendanceStatus = AttendanceStatus .None ,
878+ flags = 0L
879+ )
880+
881+ val result = testFormatter.formatNextNotificationIndicator(
882+ event = event,
883+ displayNextGCalReminder = true ,
884+ displayNextAppAlert = false ,
885+ remindersEnabled = false
886+ )
887+
888+ // Should show indicator because reminder is 1hr before THIS WEEK's instance
889+ assertNotNull(" Recurring event should show GCal indicator using instance time" , result)
890+ assertTrue(" Should contain calendar emoji" , result!! .contains(" 📅" ))
891+ assertTrue(" Should show 1h duration" , result.contains(" 1h" ))
892+ }
893+
894+ @Test
895+ fun `formatNextNotificationIndicator - zero instanceStartTime falls back via displayedStartTime` () {
896+ val eventId = 600L
897+ val futureStartTime = baseTime + 2 * Consts .HOUR_IN_MILLISECONDS
898+
899+ // Master event with future start time
900+ val mockCalendarProvider = mockk<CalendarProviderInterface >()
901+ every { mockCalendarProvider.getEvent(any(), eq(eventId)) } returns createMockEventRecord(
902+ eventId = eventId,
903+ startTime = futureStartTime,
904+ reminders = listOf (EventReminderRecord .minutes(60 )) // 1 hour before
905+ )
906+
907+ val testFormatter = EventFormatter (
908+ ctx = context,
909+ clock = testClock,
910+ calendarProvider = mockCalendarProvider
911+ )
912+
913+ // EventAlertRecord with instanceStartTime = 0L (not set)
914+ // displayedStartTime property will fall back to startTime
915+ val event = EventAlertRecord (
916+ calendarId = 1L ,
917+ eventId = eventId,
918+ isAllDay = false ,
919+ isRepeating = false ,
920+ alertTime = baseTime,
921+ notificationId = eventId.toInt(),
922+ title = " Test Event" ,
923+ desc = " " ,
924+ startTime = futureStartTime,
925+ endTime = futureStartTime + Consts .HOUR_IN_MILLISECONDS ,
926+ instanceStartTime = 0L , // NOT SET - should fall back to startTime
927+ instanceEndTime = 0L ,
928+ location = " " ,
929+ lastStatusChangeTime = baseTime,
930+ snoozedUntil = 0L ,
931+ displayStatus = EventDisplayStatus .Hidden ,
932+ color = 0 ,
933+ origin = EventOrigin .ProviderBroadcast ,
934+ timeFirstSeen = baseTime,
935+ eventStatus = EventStatus .Confirmed ,
936+ attendanceStatus = AttendanceStatus .None ,
937+ flags = 0L
938+ )
939+
940+ // Verify displayedStartTime falls back correctly
941+ assertEquals(" displayedStartTime should fall back to startTime when instanceStartTime is 0" ,
942+ futureStartTime, event.displayedStartTime)
943+
944+ val result = testFormatter.formatNextNotificationIndicator(
945+ event = event,
946+ displayNextGCalReminder = true ,
947+ displayNextAppAlert = false ,
948+ remindersEnabled = false
949+ )
950+
951+ // Should show indicator because displayedStartTime falls back to startTime
952+ assertNotNull(" Zero instanceStartTime should fall back to startTime via displayedStartTime" , result)
953+ assertTrue(" Should contain calendar emoji" , result!! .contains(" 📅" ))
954+ }
805955}
806956
0 commit comments