@@ -1562,6 +1562,132 @@ class ToDoListViewModelTest {
15621562 assertEquals(0 , viewModel.uiState.value.toDoCount)
15631563 }
15641564
1565+ @Test
1566+ fun `ViewModel handles duplicate planner items with same ID` () = runTest {
1567+ val date1 = Date (1704067200000L ) // Jan 1, 2024
1568+ val date2 = Date (1704153600000L ) // Jan 2, 2024
1569+
1570+ // Create duplicate planner items with the same ID but different dates
1571+ val plannerItems = listOf (
1572+ createPlannerItem(id = 1L , title = " Assignment 1" , plannableDate = date1),
1573+ createPlannerItem(id = 1L , title = " Assignment 1" , plannableDate = date2), // Duplicate ID!
1574+ createPlannerItem(id = 2L , title = " Assignment 2" , plannableDate = date1)
1575+ )
1576+
1577+ coEvery { repository.getCourses(any()) } returns DataResult .Success (emptyList())
1578+ coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult .Success (plannerItems)
1579+
1580+ val viewModel = getViewModel()
1581+
1582+ val uiState = viewModel.uiState.value
1583+ val allItems = uiState.itemsByDate.values.flatten()
1584+
1585+ // Should only have 2 unique items (duplicates removed)
1586+ assertEquals(2 , allItems.size)
1587+
1588+ // Verify both unique IDs are present
1589+ assertTrue(allItems.any { it.id == " 1" })
1590+ assertTrue(allItems.any { it.id == " 2" })
1591+
1592+ // Verify no duplicate IDs exist
1593+ val itemIds = allItems.map { it.id }
1594+ assertEquals(itemIds.size, itemIds.distinct().size)
1595+ }
1596+
1597+ @Test
1598+ fun `ViewModel handles multiple duplicate planner items` () = runTest {
1599+ val date = Date (1704067200000L )
1600+
1601+ // Create multiple duplicates
1602+ val plannerItems = listOf (
1603+ createPlannerItem(id = 1L , title = " Assignment 1" , plannableDate = date),
1604+ createPlannerItem(id = 1L , title = " Assignment 1" , plannableDate = date), // Duplicate
1605+ createPlannerItem(id = 1L , title = " Assignment 1" , plannableDate = date), // Duplicate
1606+ createPlannerItem(id = 2L , title = " Assignment 2" , plannableDate = date),
1607+ createPlannerItem(id = 2L , title = " Assignment 2" , plannableDate = date), // Duplicate
1608+ createPlannerItem(id = 3L , title = " Assignment 3" , plannableDate = date)
1609+ )
1610+
1611+ coEvery { repository.getCourses(any()) } returns DataResult .Success (emptyList())
1612+ coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult .Success (plannerItems)
1613+
1614+ val viewModel = getViewModel()
1615+
1616+ val uiState = viewModel.uiState.value
1617+ val allItems = uiState.itemsByDate.values.flatten()
1618+
1619+ // Should only have 3 unique items
1620+ assertEquals(3 , allItems.size)
1621+
1622+ // Verify all unique IDs are present
1623+ assertTrue(allItems.any { it.id == " 1" })
1624+ assertTrue(allItems.any { it.id == " 2" })
1625+ assertTrue(allItems.any { it.id == " 3" })
1626+
1627+ // Verify no duplicate IDs exist
1628+ val itemIds = allItems.map { it.id }
1629+ assertEquals(itemIds.size, itemIds.distinct().size)
1630+ }
1631+
1632+ @Test
1633+ fun `ViewModel handles duplicates across different date groups` () = runTest {
1634+ val date1 = Date (1704067200000L ) // Jan 1, 2024
1635+ val date2 = Date (1704153600000L ) // Jan 2, 2024
1636+
1637+ // Same assignment appearing on two different dates (backend anomaly)
1638+ val plannerItems = listOf (
1639+ createPlannerItem(id = 1L , title = " Assignment 1" , plannableDate = date1),
1640+ createPlannerItem(id = 1L , title = " Assignment 1" , plannableDate = date2), // Same ID, different date
1641+ createPlannerItem(id = 2L , title = " Assignment 2" , plannableDate = date1)
1642+ )
1643+
1644+ coEvery { repository.getCourses(any()) } returns DataResult .Success (emptyList())
1645+ coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult .Success (plannerItems)
1646+
1647+ val viewModel = getViewModel()
1648+
1649+ val uiState = viewModel.uiState.value
1650+
1651+ // Should have 2 unique items total (first occurrence of each ID kept)
1652+ val allItems = uiState.itemsByDate.values.flatten()
1653+ assertEquals(2 , allItems.size)
1654+
1655+ // Verify IDs are unique
1656+ val itemIds = allItems.map { it.id }
1657+ assertEquals(itemIds.size, itemIds.distinct().size)
1658+
1659+ // Should still have items grouped by date, but no duplicate IDs
1660+ assertTrue(uiState.itemsByDate.keys.size <= 2 )
1661+ }
1662+
1663+ @Test
1664+ fun `ViewModel preserves first occurrence when duplicates exist` () = runTest {
1665+ val date1 = Date (1704067200000L ) // Jan 1, 2024
1666+ val date2 = Date (1704153600000L ) // Jan 2, 2024
1667+
1668+ // First occurrence should be kept (Jan 1), second occurrence should be filtered out (Jan 2)
1669+ val plannerItems = listOf (
1670+ createPlannerItem(id = 1L , title = " Assignment 1" , courseId = 100L , plannableDate = date1),
1671+ createPlannerItem(id = 1L , title = " Assignment 1 Duplicate" , courseId = 200L , plannableDate = date2)
1672+ )
1673+
1674+ coEvery { repository.getCourses(any()) } returns DataResult .Success (emptyList())
1675+ coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult .Success (plannerItems)
1676+
1677+ val viewModel = getViewModel()
1678+
1679+ val uiState = viewModel.uiState.value
1680+ val allItems = uiState.itemsByDate.values.flatten()
1681+
1682+ // Should only have 1 item
1683+ assertEquals(1 , allItems.size)
1684+
1685+ // First item should be kept (the one with date1)
1686+ val item = allItems.first()
1687+ assertEquals(" 1" , item.id)
1688+ assertEquals(date1, item.date)
1689+ }
1690+
15651691 // Helper functions
15661692 private fun getViewModel (): ToDoListViewModel {
15671693 return ToDoListViewModel (context, repository, networkStateProvider, firebaseCrashlytics, toDoFilterDao, apiPrefs, analytics, toDoListViewModelBehavior, calendarSharedEvents)
0 commit comments