Coverage Summary for Class: CategoryPickerRows (com.georgeci.moneysurfer.feature.category.picker)

Class Class, % Method, % Branch, % Line, % Instruction, %
CategoryPickerRows 100% (1/1) 100% (6/6) 90.7% (49/54) 100% (33/33) 95% (322/339)


 package com.georgeci.moneysurfer.feature.category.picker
 
 import com.georgeci.moneysurfer.domain.model.Category
 import com.georgeci.moneysurfer.domain.primitives.CategoryId
 import com.georgeci.moneysurfer.domain.primitives.CategoryType
 
 /**
  * Turns the raw category list into what each picker variant renders.
  *
  * Kept as pure functions rather than folded into the composables so both shapes — the flat grid
  * and the parent/child tree — are asserted by the state tests instead of by eyeballing a sheet.
  */
 internal object CategoryPickerRows {
 
     /** Type the sheet opens on: what the caller pinned, else the selection's own type. */
     fun initialType(
         categories: List<Category>,
         selectedId: CategoryId?,
         filterType: CategoryType?,
     ): CategoryType = filterType
         ?: categories.firstOrNull { it.id == selectedId }?.type
         ?: CategoryType.EXPENSE
 
     /**
      * Parents that start expanded: the one holding the current selection, so a selected child is
      * on screen the moment the sheet opens instead of hidden behind a chevron.
      */
     fun expandedForSelection(categories: List<Category>, selectedId: CategoryId?): Set<CategoryId> =
         categories.firstOrNull { it.id == selectedId }?.parentId?.let(::setOf).orEmpty()
 
     /** Variant A: every category of [type], hierarchy flattened away, in arrival order. */
     fun grid(
         categories: List<Category>,
         type: CategoryType,
         query: String,
         selectedId: CategoryId?,
     ): List<CategoryPickerItem> = categories
         .filter { it.type == type && it.matches(query) }
         .map { it.toItem(selectedId) }
 
     /**
      * Variant B: top-level categories of [type], each carrying the children it currently shows.
      *
      * A category whose parent is missing from [categories] — filtered out by type, or not yet
      * synced — is drawn at the top level rather than dropped, the same call `CategoryTree.flatten`
      * makes on the manage screen.
      *
      * Search overrides [expandedIds]: a parent is kept when it matches or holds a match, and it is
      * force-expanded down to the matching children, since a hit the user cannot see is no hit.
      */
     fun tree(
         categories: List<Category>,
         type: CategoryType,
         query: String,
         selectedId: CategoryId?,
         expandedIds: Set<CategoryId>,
     ): List<CategoryTreeGroup> {
         val ofType = categories.filter { it.type == type }
         val present = ofType.mapTo(mutableSetOf()) { it.id }
         val childrenByParent = ofType
             .filter { it.parentId != null && it.parentId in present }
             .groupBy { requireNotNull(it.parentId) }
         val roots = ofType.filter { it.parentId == null || it.parentId !in present }
         val searching = query.isNotBlank()
 
         return roots.mapNotNull { root ->
             val children = childrenByParent[root.id].orEmpty()
             val visible = if (searching) children.filter { it.matches(query) } else children
             if (searching && !root.matches(query) && visible.isEmpty()) return@mapNotNull null
 
             val expanded = if (searching) visible.isNotEmpty() else root.id in expandedIds
             CategoryTreeGroup(
                 parent = root.toItem(selectedId),
                 childCount = children.size,
                 expanded = expanded,
                 children = if (expanded) visible.map { it.toItem(selectedId) } else emptyList(),
             )
         }
     }
 
     private fun Category.matches(query: String): Boolean =
         query.isBlank() || name.contains(query.trim(), ignoreCase = true)
 
     private fun Category.toItem(selectedId: CategoryId?) = CategoryPickerItem(
         id = id,
         name = name,
         iconKey = iconKey,
         hue = hue,
         systemKind = systemKind?.name,
         selected = id == selectedId,
     )
 }