package de.krisenvorrat.app.ui.item

import de.krisenvorrat.app.data.db.entity.CategoryEntity
import de.krisenvorrat.app.data.db.entity.ItemEntity
import de.krisenvorrat.app.data.db.entity.LocationEntity
import de.krisenvorrat.app.domain.repository.CategoryRepository
import de.krisenvorrat.app.domain.repository.ItemRepository
import de.krisenvorrat.app.domain.repository.LocationRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.time.LocalDate

@OptIn(ExperimentalCoroutinesApi::class)
class ItemListViewModelTest {

    private val testDispatcher = StandardTestDispatcher()
    private lateinit var fakeItemRepository: FakeItemRepository
    private lateinit var fakeCategoryRepository: FakeCategoryRepository
    private lateinit var fakeLocationRepository: FakeLocationRepository
    private lateinit var viewModel: ItemListViewModel

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
        fakeItemRepository = FakeItemRepository()
        fakeCategoryRepository = FakeCategoryRepository()
        fakeLocationRepository = FakeLocationRepository()
        viewModel = ItemListViewModel(
            itemRepository = fakeItemRepository,
            categoryRepository = fakeCategoryRepository,
            locationRepository = fakeLocationRepository
        )
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun test_init_withNoItems_stateIsEmpty() = runTest(testDispatcher) {
        // Given – empty repositories

        // When
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertTrue(state.isEmpty)
        assertTrue(state.groupedItems.isEmpty())
    }

    @Test
    fun test_init_withItems_itemsAreGroupedByCategory() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(
            listOf(
                CategoryEntity(id = 1, name = "Lebensmittel"),
                CategoryEntity(id = 2, name = "Hygiene")
            )
        )
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Konserve", categoryId = 1),
                buildItemEntity(id = "b", name = "Seife", categoryId = 2),
                buildItemEntity(id = "c", name = "Reis", categoryId = 1)
            )
        )

        // When
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertFalse(state.isEmpty)
        assertEquals(2, state.groupedItems.size)
        assertEquals(2, state.groupedItems["Lebensmittel"]?.size)
        assertEquals(1, state.groupedItems["Hygiene"]?.size)
    }

    @Test
    fun test_init_withItems_categoryAndLocationNamesAreResolved() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(buildItemEntity(id = "a", name = "Konserve", categoryId = 1, locationId = 1))
        )

        // When
        advanceUntilIdle()

        // Then
        val item = viewModel.uiState.value.groupedItems["Lebensmittel"]?.first()
        assertEquals("Lebensmittel", item?.categoryName)
        assertEquals("Keller", item?.locationName)
    }

    @Test
    fun test_init_withUnknownCategoryId_showsUnbekannt() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(emptyList())
        fakeLocationRepository.emit(emptyList())
        fakeItemRepository.emit(listOf(buildItemEntity(id = "a", categoryId = 99, locationId = 99)))

        // When
        advanceUntilIdle()

        // Then
        val item = viewModel.uiState.value.groupedItems["Unbekannt"]?.first()
        assertEquals("Unbekannt", item?.categoryName)
        assertEquals("Unbekannt", item?.locationName)
    }

    @Test
    fun test_showDeleteDialog_setsDialogStateCorrectly() = runTest(testDispatcher) {
        // Given
        advanceUntilIdle()
        val itemUi = buildItemUiModel(id = "a", name = "Konserve")

        // When
        viewModel.showDeleteDialog(itemUi)

        // Then
        val state = viewModel.uiState.value
        assertTrue(state.isDeleteDialogVisible)
        assertEquals(itemUi, state.itemToDelete)
    }

    @Test
    fun test_dismissDeleteDialog_clearsDialogState() = runTest(testDispatcher) {
        // Given
        viewModel.showDeleteDialog(buildItemUiModel(id = "a"))
        advanceUntilIdle()

        // When
        viewModel.dismissDeleteDialog()

        // Then
        val state = viewModel.uiState.value
        assertFalse(state.isDeleteDialogVisible)
        assertNull(state.itemToDelete)
    }

    @Test
    fun test_deleteItem_withSelectedItem_deletesAndClosesDialog() = runTest(testDispatcher) {
        // Given
        val itemUi = buildItemUiModel(id = "del1", name = "Konserve")
        viewModel.showDeleteDialog(itemUi)
        advanceUntilIdle()

        // When
        viewModel.deleteItem()
        advanceUntilIdle()

        // Then
        assertFalse(viewModel.uiState.value.isDeleteDialogVisible)
        assertNull(viewModel.uiState.value.itemToDelete)
        assertEquals(1, fakeItemRepository.deletedItems.size)
        assertEquals("del1", fakeItemRepository.deletedItems.first().id)
    }

    @Test
    fun test_deleteItem_withNoSelection_doesNothing() = runTest(testDispatcher) {
        // Given – no item selected
        advanceUntilIdle()

        // When
        viewModel.deleteItem()
        advanceUntilIdle()

        // Then
        assertTrue(fakeItemRepository.deletedItems.isEmpty())
    }

    @Test
    fun test_init_groupsAreSortedAlphabetically() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(
            listOf(
                CategoryEntity(id = 1, name = "Wasser"),
                CategoryEntity(id = 2, name = "Arzneimittel"),
                CategoryEntity(id = 3, name = "Lebensmittel")
            )
        )
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", categoryId = 1),
                buildItemEntity(id = "b", categoryId = 2),
                buildItemEntity(id = "c", categoryId = 3)
            )
        )

        // When
        advanceUntilIdle()

        // Then
        val keys = viewModel.uiState.value.groupedItems.keys.toList()
        assertEquals(listOf("Arzneimittel", "Lebensmittel", "Wasser"), keys)
    }

    // region Search & Filter Tests

    @Test
    fun test_searchByName_filtersCorrectly() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis"),
                buildItemEntity(id = "b", name = "Konserve"),
                buildItemEntity(id = "c", name = "Reismehl")
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onSearchQueryChanged("Reis")
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(2, allItems.size)
        assertTrue(allItems.all { it.name.contains("Reis") })
    }

    @Test
    fun test_searchByNotes_filtersCorrectly() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis", notes = "glutenfrei"),
                buildItemEntity(id = "b", name = "Nudeln", notes = "enthält Gluten")
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onSearchQueryChanged("glutenfrei")
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(1, allItems.size)
        assertEquals("Reis", allItems.first().name)
    }

    @Test
    fun test_searchIsCaseInsensitive() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis"),
                buildItemEntity(id = "b", name = "Nudeln")
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onSearchQueryChanged("reis")
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(1, allItems.size)
        assertEquals("Reis", allItems.first().name)
    }

    @Test
    fun test_filterByCategory_showsOnlyMatchingItems() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(
            listOf(
                CategoryEntity(id = 1, name = "Lebensmittel"),
                CategoryEntity(id = 2, name = "Hygiene")
            )
        )
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis", categoryId = 1),
                buildItemEntity(id = "b", name = "Seife", categoryId = 2)
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onCategoryFilterChanged(1)
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(1, allItems.size)
        assertEquals("Reis", allItems.first().name)
    }

    @Test
    fun test_filterByLocation_showsOnlyMatchingItems() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(
            listOf(
                LocationEntity(id = 1, name = "Keller"),
                LocationEntity(id = 2, name = "Speisekammer")
            )
        )
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis", locationId = 1),
                buildItemEntity(id = "b", name = "Nudeln", locationId = 2)
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onLocationFilterChanged(2)
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(1, allItems.size)
        assertEquals("Nudeln", allItems.first().name)
    }

    @Test
    fun test_filterByExpiryExpired_showsOnlyExpiredItems() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Abgelaufen", expiryDate = LocalDate.now().minusDays(5)),
                buildItemEntity(id = "b", name = "Frisch", expiryDate = LocalDate.now().plusDays(90))
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onExpiryFilterChanged(ExpiryFilter.EXPIRED)
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(1, allItems.size)
        assertEquals("Abgelaufen", allItems.first().name)
    }

    @Test
    fun test_filterByExpirySoon_showsOnlyExpiringSoonItems() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Abgelaufen", expiryDate = LocalDate.now().minusDays(5)),
                buildItemEntity(id = "b", name = "BaldAblaufend", expiryDate = LocalDate.now().plusDays(10)),
                buildItemEntity(id = "c", name = "Frisch", expiryDate = LocalDate.now().plusDays(90))
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onExpiryFilterChanged(ExpiryFilter.EXPIRING_SOON)
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(1, allItems.size)
        assertEquals("BaldAblaufend", allItems.first().name)
    }

    @Test
    fun test_filterByExpiryOk_showsOnlyOkItems() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Abgelaufen", expiryDate = LocalDate.now().minusDays(5)),
                buildItemEntity(id = "b", name = "Frisch", expiryDate = LocalDate.now().plusDays(90)),
                buildItemEntity(id = "c", name = "OhneDatum", expiryDate = null)
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onExpiryFilterChanged(ExpiryFilter.OK)
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(2, allItems.size)
        assertTrue(allItems.map { it.name }.containsAll(listOf("Frisch", "OhneDatum")))
    }

    @Test
    fun test_combinedSearchAndFilter_appliesBoth() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(
            listOf(
                CategoryEntity(id = 1, name = "Lebensmittel"),
                CategoryEntity(id = 2, name = "Hygiene")
            )
        )
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis", categoryId = 1),
                buildItemEntity(id = "b", name = "Reisseife", categoryId = 2),
                buildItemEntity(id = "c", name = "Nudeln", categoryId = 1)
            )
        )
        advanceUntilIdle()

        // When
        viewModel.onSearchQueryChanged("Reis")
        viewModel.onCategoryFilterChanged(1)
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(1, allItems.size)
        assertEquals("Reis", allItems.first().name)
    }

    @Test
    fun test_clearSearch_showsAllItems() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis"),
                buildItemEntity(id = "b", name = "Nudeln")
            )
        )
        advanceUntilIdle()
        viewModel.onSearchQueryChanged("Reis")
        advanceUntilIdle()
        assertEquals(1, viewModel.uiState.value.groupedItems.values.flatten().size)

        // When
        viewModel.clearSearch()
        advanceUntilIdle()

        // Then
        val allItems = viewModel.uiState.value.groupedItems.values.flatten()
        assertEquals(2, allItems.size)
    }

    @Test
    fun test_noFilterResults_hasNoFilterResultsIsTrue() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(listOf(buildItemEntity(id = "a", name = "Reis")))
        advanceUntilIdle()

        // When
        viewModel.onSearchQueryChanged("XYZ-nicht-vorhanden")
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertTrue(state.hasNoFilterResults)
        assertFalse(state.isEmpty)
        assertTrue(state.groupedItems.isEmpty())
    }

    @Test
    fun test_availableCategoriesAndLocations_arePopulated() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(
            listOf(
                CategoryEntity(id = 2, name = "Hygiene"),
                CategoryEntity(id = 1, name = "Lebensmittel")
            )
        )
        fakeLocationRepository.emit(
            listOf(
                LocationEntity(id = 2, name = "Speisekammer"),
                LocationEntity(id = 1, name = "Keller")
            )
        )
        fakeItemRepository.emit(emptyList())

        // When
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertEquals(listOf(2 to "Hygiene", 1 to "Lebensmittel"), state.availableCategories)
        assertEquals(listOf(1 to "Keller", 2 to "Speisekammer"), state.availableLocations)
    }

    @Test
    fun test_filterByCategory_thenClear_showsAllItems() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(
            listOf(
                CategoryEntity(id = 1, name = "Lebensmittel"),
                CategoryEntity(id = 2, name = "Hygiene")
            )
        )
        fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller")))
        fakeItemRepository.emit(
            listOf(
                buildItemEntity(id = "a", name = "Reis", categoryId = 1),
                buildItemEntity(id = "b", name = "Seife", categoryId = 2)
            )
        )
        advanceUntilIdle()
        viewModel.onCategoryFilterChanged(1)
        advanceUntilIdle()
        assertEquals(1, viewModel.uiState.value.groupedItems.values.flatten().size)

        // When
        viewModel.onCategoryFilterChanged(null)
        advanceUntilIdle()

        // Then
        assertEquals(2, viewModel.uiState.value.groupedItems.values.flatten().size)
    }

    // endregion
}

// region Test Helpers

private fun buildItemEntity(
    id: String = "id1",
    name: String = "Konserve",
    categoryId: Int = 1,
    locationId: Int = 1,
    notes: String = "",
    expiryDate: LocalDate? = null
) = ItemEntity(
    id = id,
    name = name,
    categoryId = categoryId,
    quantity = 2.0,
    unit = "Stk",
    unitPrice = 1.5,
    kcalPerKg = null,
    expiryDate = expiryDate,
    locationId = locationId,
    notes = notes,
    lastUpdated = 0L
)

private fun buildItemUiModel(
    id: String = "id1",
    name: String = "Konserve",
    categoryName: String = "Lebensmittel",
    locationName: String = "Keller"
) = ItemUiModel(
    id = id,
    name = name,
    categoryName = categoryName,
    quantity = 2.0,
    unit = "Stk",
    locationName = locationName,
    expiryDate = null,
    categoryId = 1
)

private class FakeItemRepository : ItemRepository {
    private val flow = MutableStateFlow<List<ItemEntity>>(emptyList())
    val deletedItems = mutableListOf<ItemEntity>()

    fun emit(items: List<ItemEntity>) {
        flow.value = items
    }

    override fun getAll(): Flow<List<ItemEntity>> = flow

    override suspend fun getById(id: String): ItemEntity? =
        flow.value.find { it.id == id }

    override suspend fun insert(item: ItemEntity) {
        flow.value = flow.value + item
    }

    override suspend fun update(item: ItemEntity) {
        flow.value = flow.value.map { if (it.id == item.id) item else it }
    }

    override suspend fun delete(item: ItemEntity) {
        deletedItems.add(item)
        flow.value = flow.value.filter { it.id != item.id }
    }

    override fun getByCategory(categoryId: Int): Flow<List<ItemEntity>> =
        MutableStateFlow(flow.value.filter { it.categoryId == categoryId })

    override fun getByLocation(locationId: Int): Flow<List<ItemEntity>> =
        MutableStateFlow(flow.value.filter { it.locationId == locationId })

    override fun getExpiringSoon(daysUntil: Int): Flow<List<ItemEntity>> =
        MutableStateFlow(emptyList())

    override suspend fun countByCategoryId(categoryId: Int): Int = 0
    override suspend fun reassignCategory(fromId: Int, toId: Int) {}
    override suspend fun countByLocationId(locationId: Int): Int = 0
    override suspend fun reassignLocation(fromId: Int, toId: Int) {}
    override suspend fun getLastUsedLocationId(): Int? = null
}

private class FakeCategoryRepository : CategoryRepository {
    private val flow = MutableStateFlow<List<CategoryEntity>>(emptyList())

    fun emit(categories: List<CategoryEntity>) {
        flow.value = categories
    }

    override fun getAll(): Flow<List<CategoryEntity>> = flow

    override suspend fun insert(category: CategoryEntity) {
        flow.value = flow.value + category
    }

    override suspend fun update(category: CategoryEntity) {
        flow.value = flow.value.map { if (it.id == category.id) category else it }
    }

    override suspend fun delete(category: CategoryEntity) {
        flow.value = flow.value.filter { it.id != category.id }
    }
}

private class FakeLocationRepository : LocationRepository {
    private val flow = MutableStateFlow<List<LocationEntity>>(emptyList())

    fun emit(locations: List<LocationEntity>) {
        flow.value = locations
    }

    override fun getAll(): Flow<List<LocationEntity>> = flow

    override suspend fun insert(location: LocationEntity) {
        flow.value = flow.value + location
    }

    override suspend fun update(location: LocationEntity) {
        flow.value = flow.value.map { if (it.id == location.id) location else it }
    }

    override suspend fun delete(location: LocationEntity) {
        flow.value = flow.value.filter { it.id != location.id }
    }
}

// endregion
