Загрузка данных


package ru.notesapp.viewmodel

import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import ru.notesapp.data.NotesRepository
import ru.notesapp.data.remote.NetworkResult
import ru.notesapp.domain.Note
import ru.notesapp.domain.NoteType
import ru.notesapp.util.MainDispatcherRule

@OptIn(ExperimentalCoroutinesApi::class)
class NotesViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private lateinit var repo: NotesRepository

    private fun note(
        id: Long = 1L,
        title: String = "t",
        content: String = "c",
        type: NoteType = NoteType.Text,
        imageUrl: String? = null,
    ) = Note(id, title, content, 0L, type, imageUrl)

    @Before
    fun setUp() {
        repo = mockk(relaxed = true)
    }

    @After
    fun tearDown() {
        io.mockk.clearAllMocks()
    }

    // ---------- A. Initial state (5) ----------

    @Test
    fun initial_uiState_is_Loading() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        assertEquals(NotesUiState.Loading, vm.uiState.value)
    }

    @Test
    fun initial_isRefreshing_is_false() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        assertFalse(vm.isRefreshing.value)
    }

    @Test
    fun initial_snackbarMessage_is_null() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        assertNull(vm.snackbarMessage.value)
    }

    @Test
    fun init_calls_observeAll() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        NotesViewModel(repo)
        advanceUntilIdle()
        verify(atLeast = 1) { repo.observeAll() }
    }

    @Test
    fun init_calls_observeAll_once() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        NotesViewModel(repo)
        advanceUntilIdle()
        verify(exactly = 1) { repo.observeAll() }
    }

    // ---------- B. observeAll → UiState mapping (8) ----------

    @Test
    fun observeAll_empty_emits_Empty() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        assertEquals(NotesUiState.Empty, vm.uiState.value)
    }

    @Test
    fun observeAll_single_emits_Success() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(listOf(note()))
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        assertTrue(vm.uiState.value is NotesUiState.Success)
    }

    @Test
    fun observeAll_multiple_emits_Success_with_list() = runTest(mainDispatcherRule.dispatcher) {
        val notes = listOf(note(1L), note(2L), note(3L))
        every { repo.observeAll() } returns flowOf(notes)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        val state = vm.uiState.value as NotesUiState.Success
        assertEquals(3, state.notes.size)
    }

    @Test
    fun observeAll_state_transitions_Loading_to_Empty() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        assertEquals(NotesUiState.Loading, vm.uiState.value)
        advanceUntilIdle()
        assertEquals(NotesUiState.Empty, vm.uiState.value)
    }

    @Test
    fun observeAll_state_transitions_Loading_to_Success() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(listOf(note()))
        val vm = NotesViewModel(repo)
        assertEquals(NotesUiState.Loading, vm.uiState.value)
        advanceUntilIdle()
        assertTrue(vm.uiState.value is NotesUiState.Success)
    }

    @Test
    fun observeAll_updates_on_new_emission() = runTest(mainDispatcherRule.dispatcher) {
        val flow = flow {
            emit(listOf(note(1L)))
            delay(10)
            emit(listOf(note(1L), note(2L)))
        }
        every { repo.observeAll() } returns flow
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        val state = vm.uiState.value as NotesUiState.Success
        assertEquals(2, state.notes.size)
    }

    @Test
    fun observeAll_multiple_emissions_collects_all() = runTest(mainDispatcherRule.dispatcher) {
        var emissionCount = 0
        val flow = flow {
            emissionCount++
            emit(emptyList())
            emissionCount++
            emit(listOf(note()))
        }
        every { repo.observeAll() } returns flow
        NotesViewModel(repo)
        advanceUntilIdle()
        assertEquals(2, emissionCount)
    }

    @Test
    fun observeAll_second_emission_replaces_state() = runTest(mainDispatcherRule.dispatcher) {
        val flow = flow {
            emit(listOf(note(1L)))
            emit(emptyList())
        }
        every { repo.observeAll() } returns flow
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        assertEquals(NotesUiState.Empty, vm.uiState.value)
    }

    // ---------- C. addNote (8) ----------

    @Test
    fun addNote_calls_repo_add_once() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("t", "c", NoteType.Text)
        advanceUntilIdle()
        coVerify(exactly = 1) { repo.add(any()) }
    }

    @Test
    fun addNote_passes_title() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("Заголовок", "c", NoteType.Text)
        advanceUntilIdle()
        coVerify { repo.add(match { it.title == "Заголовок" }) }
    }

    @Test
    fun addNote_passes_content() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("t", "Содержание", NoteType.Text)
        advanceUntilIdle()
        coVerify { repo.add(match { it.content == "Содержание" }) }
    }

    @Test
    fun addNote_passes_Text_type_when_no_url() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("t", "c", NoteType.Text, null)
        advanceUntilIdle()
        coVerify { repo.add(match { it.type is NoteType.Text }) }
    }

    @Test
    fun addNote_passes_Image_type_when_url_provided() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("t", "c", NoteType.Image, "https://x/y")
        advanceUntilIdle()
        coVerify { repo.add(match { it.type is NoteType.Image }) }
    }

    @Test
    fun addNote_passes_imageUrl() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("t", "c", NoteType.Image, "https://example.com/img.png")
        advanceUntilIdle()
        coVerify { repo.add(match { it.imageUrl == "https://example.com/img.png" }) }
    }

    @Test
    fun addNote_with_empty_title_still_calls_repo() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("", "c", NoteType.Text)
        advanceUntilIdle()
        coVerify(exactly = 1) { repo.add(any()) }
    }

    @Test
    fun addNote_createdAt_close_to_now() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.add(any()) } returns NetworkResult.Success(1L)
        val before = System.currentTimeMillis()
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.addNote("t", "c", NoteType.Text)
        advanceUntilIdle()
        val after = System.currentTimeMillis()
        coVerify { repo.add(match { it.createdAt in before..after }) }
    }

    // ---------- D. deleteNote (5) ----------

    @Test
    fun deleteNote_calls_repo_delete() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.delete(any()) } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.deleteNote(1L)
        advanceUntilIdle()
        coVerify(exactly = 1) { repo.delete(1L) }
    }

    @Test
    fun deleteNote_passes_correct_id() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.delete(any()) } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.deleteNote(42L)
        advanceUntilIdle()
        coVerify { repo.delete(42L) }
    }

    @Test
    fun deleteNote_with_zero_id_calls_repo() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.delete(any()) } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.deleteNote(0L)
        advanceUntilIdle()
        coVerify { repo.delete(0L) }
    }

    @Test
    fun deleteNote_with_negative_id_calls_repo() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.delete(any()) } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.deleteNote(-1L)
        advanceUntilIdle()
        coVerify { repo.delete(-1L) }
    }

    @Test
    fun deleteNote_does_not_touch_snackbar() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.delete(any()) } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.deleteNote(1L)
        advanceUntilIdle()
        assertNull(vm.snackbarMessage.value)
    }

    // ---------- E. refresh (10) ----------

    @Test
    fun refresh_calls_repo_refresh() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        coVerify(exactly = 1) { repo.refresh() }
    }

    @Test
    fun refresh_sets_isRefreshing_false_after() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        assertFalse(vm.isRefreshing.value)
    }

    @Test
    fun refresh_success_snackbar_contains_обновлено() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        val msg = vm.snackbarMessage.value
        assertNotNull(msg)
        assertTrue(msg!!.contains("Обновлено"))
    }

    @Test
    fun refresh_success_snackbar_contains_count() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(listOf(note(1L), note(2L)))
        coEvery { repo.refresh() } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        assertTrue(vm.snackbarMessage.value!!.contains("2"))
    }

    @Test
    fun refresh_Error_emits_snackbar_with_code() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.Error(500, "boom")
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        val msg = vm.snackbarMessage.value!!
        assertTrue(msg.contains("500"))
    }

    @Test
    fun refresh_NetworkError_emits_internet_message() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.NetworkError
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        val msg = vm.snackbarMessage.value!!
        assertTrue(msg.contains("интернет", ignoreCase = true))
    }

    @Test
    fun refresh_sets_isRefreshing_true_during() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } coAnswers {
            delay(100)
            NetworkResult.Success(Unit)
        }
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        assertTrue(vm.isRefreshing.value)
        advanceUntilIdle()
        assertFalse(vm.isRefreshing.value)
    }

    @Test
    fun refresh_second_call_after_first_works() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        coVerify(exactly = 2) { repo.refresh() }
    }

    @Test
    fun refresh_does_not_throw_on_error() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.Error(400, "bad")
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        // если не упало — тест прошёл
        assertTrue(true)
    }

    @Test
    fun refresh_does_not_throw_on_network_error() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.NetworkError
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        assertTrue(true)
    }

    // ---------- F. clearSnackbarMessage (3) ----------

    @Test
    fun clearSnackbarMessage_sets_null() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.Success(Unit)
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        assertNotNull(vm.snackbarMessage.value)
        vm.clearSnackbarMessage()
        assertNull(vm.snackbarMessage.value)
    }

    @Test
    fun clearSnackbarMessage_when_null_stays_null() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.clearSnackbarMessage()
        assertNull(vm.snackbarMessage.value)
    }

    @Test
    fun clearSnackbarMessage_after_error_clears() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        coEvery { repo.refresh() } returns NetworkResult.NetworkError
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.refresh()
        advanceUntilIdle()
        vm.clearSnackbarMessage()
        assertNull(vm.snackbarMessage.value)
    }

    // ---------- G. Edge cases (4) ----------

    @Test
    fun snackbar_flow_test_emits_initial_null() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        vm.snackbarMessage.test {
            assertNull(awaitItem())
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun isRefreshing_flow_test_emits_initial_false() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        vm.isRefreshing.test {
            assertFalse(awaitItem())
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun uiState_flow_test_emits_Loading_first() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        vm.uiState.test {
            assertEquals(NotesUiState.Loading, awaitItem())
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun viewModel_scope_can_be_cancelled() = runTest(mainDispatcherRule.dispatcher) {
        every { repo.observeAll() } returns flowOf(emptyList())
        val vm = NotesViewModel(repo)
        advanceUntilIdle()
        vm.viewModelScope.cancel()
        advanceUntilIdle()
        assertTrue(true)
    }
}