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


package ru.notesapp.data.local

import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import app.cash.turbine.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import ru.notesapp.domain.NoteType

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class NoteDaoTest {

    private lateinit var db: AppDatabase
    private lateinit var dao: NoteDao

    private fun entity(
        id: Long = 0L,
        title: String = "title",
        content: String = "content",
        createdAt: Long = 1000L,
        updatedAt: Long = 0L,
        imageUrl: String? = null,
        type: NoteType = NoteType.Text,
    ) = NoteEntity(id, title, content, createdAt, updatedAt, imageUrl, type)

    @Before
    fun setUp() {
        db = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java
        )
            .allowMainThreadQueries()
            .build()
        dao = db.noteDao()
    }

    @After
    fun tearDown() {
        db.close()
    }

    // ============================================================
    // 1. observeAll() — 8 тестов
    // ============================================================

    @Test
    fun observeAll_empty_db_emits_empty_list() = runTest {
        dao.observeAll().test {
            assertEquals(emptyList<NoteEntity>(), awaitItem())
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun observeAll_single_insert_emits_list_with_one_item() = runTest {
        dao.insert(entity(title = "A"))
        dao.observeAll().test {
            assertEquals(1, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun observeAll_two_inserts_emits_two_items() = runTest {
        dao.insert(entity(title = "A"))
        dao.insert(entity(title = "B"))
        dao.observeAll().test {
            assertEquals(2, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun observeAll_sorted_by_created_at_desc() = runTest {
        dao.insert(entity(title = "old", createdAt = 100L))
        dao.insert(entity(title = "new", createdAt = 500L))
        dao.insert(entity(title = "mid", createdAt = 300L))
        dao.observeAll().test {
            val list = awaitItem()
            assertEquals(listOf("new", "mid", "old"), list.map { it.title })
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun observeAll_with_same_created_at_preserves_all_items() = runTest {
        dao.insert(entity(title = "A", createdAt = 100L))
        dao.insert(entity(title = "B", createdAt = 100L))
        dao.observeAll().test {
            assertEquals(2, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun observeAll_after_delete_emits_updated_list() = runTest {
        val id = dao.insert(entity(title = "A"))
        dao.observeAll().test {
            assertEquals(1, awaitItem().size)
            dao.deleteById(id)
            assertEquals(0, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun observeAll_after_update_emits_updated_fields() = runTest {
        val id = dao.insert(entity(title = "old"))
        dao.observeAll().test {
            val before = awaitItem().first()
            assertEquals("old", before.title)
            dao.update(before.copy(title = "new"))
            val after = awaitItem().first()
            assertEquals("new", after.title)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun observeAll_concurrent_inserts_no_data_loss() = runTest {
        repeat(10) { i ->
            dao.insert(entity(title = "n$i", createdAt = i.toLong()))
        }
        dao.observeAll().test {
            assertEquals(10, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    // ============================================================
    // 2. getById() — 6 тестов
    // ============================================================

    @Test
    fun getById_existing_returns_note() = runTest {
        val id = dao.insert(entity(title = "A"))
        val found = dao.getById(id)
        assertNotNull(found)
        assertEquals("A", found!!.title)
    }

    @Test
    fun getById_nonexistent_returns_null() = runTest {
        assertNull(dao.getById(999L))
    }

    @Test
    fun getById_zero_id_returns_null() = runTest {
        assertNull(dao.getById(0L))
    }

    @Test
    fun getById_negative_id_returns_null() = runTest {
        assertNull(dao.getById(-1L))
    }

    @Test
    fun getById_max_long_id_returns_null() = runTest {
        assertNull(dao.getById(Long.MAX_VALUE))
    }

    @Test
    fun getById_after_update_returns_updated_note() = runTest {
        val id = dao.insert(entity(title = "before"))
        val n = dao.getById(id)!!
        dao.update(n.copy(title = "after"))
        assertEquals("after", dao.getById(id)!!.title)
    }

    // ============================================================
    // 3. insert() — 8 тестов
    // ============================================================

    @Test
    fun insert_returns_new_id() = runTest {
        val id = dao.insert(entity())
        assertTrue(id > 0)
    }

    @Test
    fun insert_increments_id_sequence() = runTest {
        val id1 = dao.insert(entity())
        val id2 = dao.insert(entity())
        assertTrue(id2 > id1)
    }

    @Test
    fun insert_with_explicit_id_uses_it() = runTest {
        val id = dao.insert(entity(id = 42L))
        assertEquals(42L, id)
        assertEquals(42L, dao.getById(42L)!!.id)
    }

    @Test
    fun insert_replaces_on_conflict_when_strategy_replace() = runTest {
        dao.insert(entity(id = 1L, title = "old"))
        dao.insert(entity(id = 1L, title = "new"))
        assertEquals("new", dao.getById(1L)!!.title)
        assertEquals(1, dao.observeAll().test { awaitItem().size; cancelAndIgnoreRemainingEvents() })
    }

    @Test
    fun insert_after_replace_count_unchanged() = runTest {
        dao.insert(entity(id = 1L))
        dao.insert(entity(id = 1L))
        dao.observeAll().test {
            assertEquals(1, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun insert_empty_title_succeeds() = runTest {
        val id = dao.insert(entity(title = ""))
        assertEquals("", dao.getById(id)!!.title)
    }

    @Test
    fun insert_empty_content_succeeds() = runTest {
        val id = dao.insert(entity(content = ""))
        assertEquals("", dao.getById(id)!!.content)
    }

    @Test
    fun insert_negative_created_at_succeeds() = runTest {
        val id = dao.insert(entity(createdAt = -1000L))
        assertEquals(-1000L, dao.getById(id)!!.createdAt)
    }

    // ============================================================
    // 4. update() — 6 тестов
    // ============================================================

    @Test
    fun update_changes_title() = runTest {
        val id = dao.insert(entity(title = "old"))
        val n = dao.getById(id)!!
        dao.update(n.copy(title = "new"))
        assertEquals("new", dao.getById(id)!!.title)
    }

    @Test
    fun update_changes_content() = runTest {
        val id = dao.insert(entity(content = "old"))
        val n = dao.getById(id)!!
        dao.update(n.copy(content = "new"))
        assertEquals("new", dao.getById(id)!!.content)
    }

    @Test
    fun update_changes_type() = runTest {
        val id = dao.insert(entity(type = NoteType.Text))
        val n = dao.getById(id)!!
        dao.update(n.copy(type = NoteType.Image))
        assertTrue(dao.getById(id)!!.type is NoteType.Image)
    }

    @Test
    fun update_changes_image_url() = runTest {
        val id = dao.insert(entity(imageUrl = null))
        val n = dao.getById(id)!!
        dao.update(n.copy(imageUrl = "https://x/y.png"))
        assertEquals("https://x/y.png", dao.getById(id)!!.imageUrl)
    }

    @Test
    fun update_nonexistent_id_no_effect() = runTest {
        // Room @Update на несуществующей записи ничего не делает, не падает
        dao.update(entity(id = 999L, title = "ghost"))
        assertNull(dao.getById(999L))
    }

    @Test
    fun update_preserves_id_and_created_at() = runTest {
        val id = dao.insert(entity(title = "A", createdAt = 555L))
        val n = dao.getById(id)!!
        dao.update(n.copy(title = "B"))
        val updated = dao.getById(id)!!
        assertEquals(id, updated.id)
        assertEquals(555L, updated.createdAt)
    }

    // ============================================================
    // 5. deleteById() — 5 тестов
    // ============================================================

    @Test
    fun deleteById_existing_removes_from_db() = runTest {
        val id = dao.insert(entity())
        dao.deleteById(id)
        assertNull(dao.getById(id))
    }

    @Test
    fun deleteById_nonexistent_does_nothing() = runTest {
        dao.deleteById(999L)
        // просто не падает
        assertTrue(true)
    }

    @Test
    fun deleteById_zero_id_no_effect() = runTest {
        dao.insert(entity(id = 1L))
        dao.deleteById(0L)
        assertNotNull(dao.getById(1L))
    }

    @Test
    fun deleteById_after_delete_getById_returns_null() = runTest {
        val id = dao.insert(entity())
        dao.deleteById(id)
        assertNull(dao.getById(id))
    }

    @Test
    fun deleteById_does_not_affect_other_notes() = runTest {
        val id1 = dao.insert(entity(title = "A"))
        dao.insert(entity(title = "B"))
        dao.deleteById(id1)
        dao.observeAll().test {
            val list = awaitItem()
            assertEquals(1, list.size)
            assertEquals("B", list.first().title)
            cancelAndIgnoreRemainingEvents()
        }
    }

    // ============================================================
    // 6. searchByTitle() — 7 тестов
    // ============================================================

    @Test
    fun searchByTitle_exact_match_returns_note() = runTest {
        dao.insert(entity(title = "Kotlin"))
        dao.searchByTitle("Kotlin").test {
            val list = awaitItem()
            assertEquals(1, list.size)
            assertEquals("Kotlin", list.first().title)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun searchByTitle_partial_match_returns_note() = runTest {
        dao.insert(entity(title = "Hello World"))
        dao.searchByTitle("World").test {
            assertEquals(1, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun searchByTitle_case_insensitive() = runTest {
        dao.insert(entity(title = "KOTLIN"))
        dao.searchByTitle("kotlin").test {
            assertEquals(1, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun searchByTitle_empty_query_returns_all() = runTest {
        dao.insert(entity(title = "A"))
        dao.insert(entity(title = "B"))
        dao.searchByTitle("").test {
            assertEquals(2, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun searchByTitle_no_matches_returns_empty() = runTest {
        dao.insert(entity(title = "Hello"))
        dao.searchByTitle("xyz").test {
            assertEquals(emptyList<NoteEntity>(), awaitItem())
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun searchByTitle_special_characters_in_query() = runTest {
        dao.insert(entity(title = "note%test"))
        // % — спецсимвол LIKE, но в нашем DAO он работает как wildcard
        dao.searchByTitle("note%test").test {
            // должно найтись (LIKE без экранирования)
            assertTrue(awaitItem().isNotEmpty())
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun searchByTitle_unicode_cyrillic_query() = runTest {
        dao.insert(entity(title = "Привет мир"))
        dao.searchByTitle("Привет").test {
            assertEquals(1, awaitItem().size)
            cancelAndIgnoreRemainingEvents()
        }
    }
}