package com.clean.scanner.ui import com.clean.scanner.domain.ScanResult import com.clean.scanner.testutil.MainDispatcherRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class ScannerViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun onScan_firstSingleScan_updatesStateAndSaves() = runTest { val saved = mutableListOf>() var now = 1_000L val viewModel = ScannerViewModel( saveScan = { content, type -> saved += content to type }, nowProvider = { now } ) viewModel.onScan(ScanResult(content = "https://example.com", type = "URL")) advanceUntilIdle() val state = viewModel.uiState.value assertEquals("https://example.com", state.lastResult?.content) assertEquals(false, state.analysisEnabled) assertEquals(1, state.scanFeedbackNonce) assertEquals(0, state.duplicateFeedbackNonce) assertEquals(listOf("URL|https://example.com"), state.recentScanKeys) assertEquals(listOf("https://example.com" to "URL"), saved) } @Test fun duplicateScan_afterResume_incrementsDuplicateFeedback() = runTest { val saved = mutableListOf>() var now = 1_000L val viewModel = ScannerViewModel( saveScan = { content, type -> saved += content to type }, nowProvider = { now } ) val result = ScanResult(content = "ABC", type = "Text") viewModel.onScan(result) viewModel.resumeScanning() now = 4_000L viewModel.onScan(result) advanceUntilIdle() val state = viewModel.uiState.value assertEquals(2, state.scanFeedbackNonce) assertEquals(1, state.duplicateFeedbackNonce) assertEquals(2, saved.size) } @Test fun batchMode_addsUniqueKeepsNewestFirst_andClearWorks() = runTest { val saved = mutableListOf>() var now = 1_000L val viewModel = ScannerViewModel( saveScan = { content, type -> saved += content to type }, nowProvider = { now } ) viewModel.setBatchMode(true) viewModel.onScan(ScanResult(content = "A", type = "Text")) now = 2_000L viewModel.onScan(ScanResult(content = "B", type = "Text")) now = 3_600L viewModel.onScan(ScanResult(content = "A", type = "Text")) advanceUntilIdle() val state = viewModel.uiState.value assertEquals(2, state.batchResults.size) assertEquals("B", state.batchResults[0].result.content) assertEquals(2_000L, state.batchResults[0].timestamp) assertEquals("A", state.batchResults[1].result.content) assertEquals(1, state.duplicateFeedbackNonce) assertEquals(3, state.scanFeedbackNonce) assertTrue(state.analysisEnabled) viewModel.clearBatchResults() assertTrue(viewModel.uiState.value.batchResults.isEmpty()) assertEquals(3, saved.size) } @Test fun sameCodeWithinHoldoff_isIgnored() = runTest { val saved = mutableListOf>() var now = 1_000L val viewModel = ScannerViewModel( saveScan = { content, type -> saved += content to type }, nowProvider = { now } ) val result = ScanResult(content = "HOLD", type = "Text") viewModel.setBatchMode(true) viewModel.onScan(result) now = 2_000L viewModel.onScan(result) advanceUntilIdle() val state = viewModel.uiState.value assertEquals(1, state.scanFeedbackNonce) assertEquals(1, saved.size) assertEquals(1, state.batchResults.size) } }