package com.peachpasswords.ui.vault import androidx.lifecycle.SavedStateHandle import com.peachpasswords.ui.PeachNav import com.peachpasswords.vault.StoredVault import com.peachpasswords.vault.VaultEntry import com.peachpasswords.vault.VaultRepository import com.peachpasswords.vault.VaultSettings import com.peachpasswords.vault.VaultStorage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class NotesModeTest { private val dispatcher = StandardTestDispatcher() @Before fun setUp() = Dispatchers.setMain(dispatcher) @After fun tearDown() = Dispatchers.resetMain() private class FakeStorage : VaultStorage { var slotA: StoredVault? = null var slotB: String? = null private var ver: Long = 0 private var settings = VaultSettings() override fun loadSlotA() = slotA override fun hasVault() = slotA != null override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true } override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true } var decoy: StoredVault? = null override fun loadDecoySlot() = decoy override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true } override fun hasDecoySlot() = decoy != null override fun clearDecoySlot() { decoy = null } override fun version() = ver override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver } override fun loadSettings() = settings override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true } override fun clearVault() { slotA = null; slotB = null; ver = 0; settings = VaultSettings() } } private fun note(title: String = "n", notes: String? = null) = VaultEntry(id = "", title = title, type = "note", notes = notes, createdAt = 0, modifiedAt = 0) private fun login(title: String = "l") = VaultEntry(id = "", title = title, type = "login", password = "pw", createdAt = 0, modifiedAt = 0) // ── Navigation decision (the VaultListScreen row-tap mapping) ────────────── @Test fun note_entry_navigates_to_notes_mode() { val e = VaultEntry(id = "abc", title = "Diary", type = "note", createdAt = 0, modifiedAt = 0) assertEquals(PeachNav.NotesMode("abc"), entryTapDestination(e)) } @Test fun non_note_entry_navigates_to_entry_detail() { val e = VaultEntry(id = "xyz", title = "GitHub", type = "login", createdAt = 0, modifiedAt = 0) assertEquals(PeachNav.EntryDetail("xyz"), entryTapDestination(e)) } // ── NOTES filter pill ───────────────────────────────────────────────────── @Test fun notes_filter_shows_only_note_entries() { val list = listOf(login("a"), note("b"), login("c"), note("d")) assertEquals(listOf("b", "d"), filterNotesOnly(list, noteOnly = true).map { it.title }) // Off: everything passes through, order preserved. assertEquals(listOf("a", "b", "c", "d"), filterNotesOnly(list, noteOnly = false).map { it.title }) } // ── Auto-save debounce (NotesModeViewModel) ─────────────────────────────── @Test fun notes_auto_save_debounces_800ms() = runTest(dispatcher) { val repo = VaultRepository(FakeStorage()) repo.createVault("correct horse battery") val saved = repo.addEntry(note("My note")) val vm = NotesModeViewModel(repo, SavedStateHandle(mapOf("id" to saved.id))) vm.onContentChanged("hello") advanceTimeBy(799) runCurrent() assertNull("must not save before 800ms", repo.getEntry(saved.id)?.notes) advanceTimeBy(2) // cross the 800ms threshold advanceUntilIdle() assertEquals("hello", repo.getEntry(saved.id)?.notes) } @Test fun notes_auto_save_coalesces_rapid_keystrokes() = runTest(dispatcher) { val repo = VaultRepository(FakeStorage()) repo.createVault("correct horse battery") val saved = repo.addEntry(note("My note")) val vm = NotesModeViewModel(repo, SavedStateHandle(mapOf("id" to saved.id))) vm.onContentChanged("h") advanceTimeBy(400) vm.onContentChanged("he") // resets the timer advanceTimeBy(400) runCurrent() assertNull("timer reset by second keystroke", repo.getEntry(saved.id)?.notes) advanceUntilIdle() assertEquals("he", repo.getEntry(saved.id)?.notes) } @Test fun notes_title_edit_saves_on_blur() = runTest(dispatcher) { val repo = VaultRepository(FakeStorage()) repo.createVault("correct horse battery") val saved = repo.addEntry(note("untitled")) val vm = NotesModeViewModel(repo, SavedStateHandle(mapOf("id" to saved.id))) // saveTitleNow bypasses the 800ms debounce (fired on field blur / back press). vm.saveTitleNow("Trip plans") advanceUntilIdle() assertEquals("Trip plans", repo.getEntry(saved.id)?.title) } }