package de.krisenvorrat.app.ui.category

import de.krisenvorrat.app.data.db.entity.CategoryEntity
import de.krisenvorrat.app.data.db.entity.ItemEntity
import de.krisenvorrat.app.domain.repository.CategoryRepository
import de.krisenvorrat.app.domain.repository.ItemRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
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

@OptIn(ExperimentalCoroutinesApi::class)
class CategoryListViewModelTest {

    private val testDispatcher = StandardTestDispatcher()
    private lateinit var fakeRepository: FakeCategoryRepository
    private lateinit var fakeItemRepository: FakeItemRepositoryForCategories
    private lateinit var viewModel: CategoryListViewModel

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
        fakeRepository = FakeCategoryRepository()
        fakeItemRepository = FakeItemRepositoryForCategories()
        viewModel = CategoryListViewModel(fakeRepository, fakeItemRepository)
    }

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

    @Test
    fun test_init_withEmptyRepository_categoriesListIsEmpty() = runTest(testDispatcher) {
        // Given – fresh ViewModel

        // When
        advanceUntilIdle()

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

    @Test
    fun test_init_withExistingCategories_categoriesAreLoaded() = runTest(testDispatcher) {
        // Given
        fakeRepository.emit(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))

        // When
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertEquals(1, state.categories.size)
        assertEquals("Lebensmittel", state.categories.first().name)
    }

    @Test
    fun test_showAddDialog_setsIsAddDialogVisibleToTrue() = runTest(testDispatcher) {
        // Given
        advanceUntilIdle()

        // When
        viewModel.showAddDialog()

        // Then
        assertTrue(viewModel.uiState.value.isAddDialogVisible)
        assertEquals("", viewModel.uiState.value.newCategoryName)
    }

    @Test
    fun test_dismissAddDialog_setsIsAddDialogVisibleToFalse() = runTest(testDispatcher) {
        // Given
        viewModel.showAddDialog()
        advanceUntilIdle()

        // When
        viewModel.dismissAddDialog()

        // Then
        assertFalse(viewModel.uiState.value.isAddDialogVisible)
    }

    @Test
    fun test_updateNewCategoryName_updatesStateCorrectly() = runTest(testDispatcher) {
        // Given
        viewModel.showAddDialog()
        advanceUntilIdle()

        // When
        viewModel.updateNewCategoryName("Hygiene")

        // Then
        assertEquals("Hygiene", viewModel.uiState.value.newCategoryName)
    }

    @Test
    fun test_addCategory_withValidName_insertsAndClosesDialog() = runTest(testDispatcher) {
        // Given
        viewModel.showAddDialog()
        viewModel.updateNewCategoryName("Hygiene")
        advanceUntilIdle()

        // When
        viewModel.addCategory()
        advanceUntilIdle()

        // Then
        assertFalse(viewModel.uiState.value.isAddDialogVisible)
        assertEquals("", viewModel.uiState.value.newCategoryName)
        assertEquals(1, fakeRepository.insertedCategories.size)
        assertEquals("Hygiene", fakeRepository.insertedCategories.first().name)
    }

    @Test
    fun test_addCategory_withBlankName_doesNotInsert() = runTest(testDispatcher) {
        // Given
        viewModel.showAddDialog()
        viewModel.updateNewCategoryName("   ")
        advanceUntilIdle()

        // When
        viewModel.addCategory()
        advanceUntilIdle()

        // Then
        assertTrue(fakeRepository.insertedCategories.isEmpty())
    }

    @Test
    fun test_showDeleteDialog_setsDialogStateCorrectly() = runTest(testDispatcher) {
        // Given
        val category = CategoryEntity(id = 1, name = "Lebensmittel")
        advanceUntilIdle()

        // When
        viewModel.showDeleteDialog(category)
        advanceUntilIdle()

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

    @Test
    fun test_dismissDeleteDialog_clearsDialogState() = runTest(testDispatcher) {
        // Given
        viewModel.showDeleteDialog(CategoryEntity(id = 1, name = "Test"))
        advanceUntilIdle()

        // When
        viewModel.dismissDeleteDialog()

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

    @Test
    fun test_deleteCategory_withSelectedCategory_deletesAndClosesDialog() = runTest(testDispatcher) {
        // Given
        val category = CategoryEntity(id = 1, name = "Lebensmittel")
        viewModel.showDeleteDialog(category)
        advanceUntilIdle()

        // When
        viewModel.deleteCategory()
        advanceUntilIdle()

        // Then
        assertFalse(viewModel.uiState.value.isDeleteDialogVisible)
        assertNull(viewModel.uiState.value.categoryToDelete)
        assertEquals(1, fakeRepository.deletedCategories.size)
        assertEquals(category, fakeRepository.deletedCategories.first())
    }

    @Test
    fun test_deleteCategory_withNoSelection_doesNothing() = runTest(testDispatcher) {
        // Given – no category selected
        advanceUntilIdle()

        // When
        viewModel.deleteCategory()
        advanceUntilIdle()

        // Then
        assertTrue(fakeRepository.deletedCategories.isEmpty())
    }

    @Test
    fun test_showEditDialog_setsEditDialogState() = runTest(testDispatcher) {
        // Given
        val category = CategoryEntity(id = 1, name = "Lebensmittel")
        advanceUntilIdle()

        // When
        viewModel.showEditDialog(category)

        // Then
        val state = viewModel.uiState.value
        assertTrue(state.isEditDialogVisible)
        assertEquals(category, state.categoryToEdit)
        assertEquals("Lebensmittel", state.editCategoryName)
    }

    @Test
    fun test_dismissEditDialog_clearsEditDialogState() = runTest(testDispatcher) {
        // Given
        viewModel.showEditDialog(CategoryEntity(id = 1, name = "Test"))
        advanceUntilIdle()

        // When
        viewModel.dismissEditDialog()

        // Then
        val state = viewModel.uiState.value
        assertFalse(state.isEditDialogVisible)
        assertNull(state.categoryToEdit)
        assertEquals("", state.editCategoryName)
    }

    @Test
    fun test_saveEditCategory_withValidName_updatesAndClosesDialog() = runTest(testDispatcher) {
        // Given
        val category = CategoryEntity(id = 1, name = "Alt")
        viewModel.showEditDialog(category)
        viewModel.updateEditCategoryName("Neu")
        advanceUntilIdle()

        // When
        viewModel.saveEditCategory()
        advanceUntilIdle()

        // Then
        assertFalse(viewModel.uiState.value.isEditDialogVisible)
        assertNull(viewModel.uiState.value.categoryToEdit)
        assertEquals(1, fakeRepository.updatedCategories.size)
        assertEquals("Neu", fakeRepository.updatedCategories.first().name)
    }

    @Test
    fun test_saveEditCategory_withBlankName_doesNotUpdate() = runTest(testDispatcher) {
        // Given
        val category = CategoryEntity(id = 1, name = "Alt")
        viewModel.showEditDialog(category)
        viewModel.updateEditCategoryName("   ")
        advanceUntilIdle()

        // When
        viewModel.saveEditCategory()
        advanceUntilIdle()

        // Then
        assertTrue(fakeRepository.updatedCategories.isEmpty())
    }

    @Test
    fun test_showDeleteDialog_unusedCategory_showsSimpleDeleteDialog() = runTest(testDispatcher) {
        // Given – no items in fake item repository
        val category = CategoryEntity(id = 1, name = "Lebensmittel")
        advanceUntilIdle()

        // When
        viewModel.showDeleteDialog(category)
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertTrue(state.isDeleteDialogVisible)
        assertFalse(state.isReassignDialogVisible)
        assertEquals(category, state.categoryToDelete)
    }

    @Test
    fun test_showDeleteDialog_usedCategory_showsReassignDialog() = runTest(testDispatcher) {
        // Given – item uses category 1
        fakeItemRepository.addItem(
            ItemEntity(
                id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
                unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
                locationId = 1, notes = "", lastUpdated = 0L
            )
        )
        val category = CategoryEntity(id = 1, name = "Lebensmittel")
        advanceUntilIdle()

        // When
        viewModel.showDeleteDialog(category)
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertFalse(state.isDeleteDialogVisible)
        assertTrue(state.isReassignDialogVisible)
        assertEquals(1, state.itemCountForDelete)
        assertEquals(category, state.categoryToDelete)
    }

    @Test
    fun test_deleteWithReassignment_reassignsItemsAndDeletesCategory() = runTest(testDispatcher) {
        // Given
        fakeItemRepository.addItem(
            ItemEntity(
                id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
                unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
                locationId = 1, notes = "", lastUpdated = 0L
            )
        )
        val category = CategoryEntity(id = 1, name = "Lebensmittel")
        viewModel.showDeleteDialog(category)
        advanceUntilIdle()
        viewModel.updateReassignTarget(2)

        // When
        viewModel.deleteWithReassignment()
        advanceUntilIdle()

        // Then
        assertFalse(viewModel.uiState.value.isReassignDialogVisible)
        assertNull(viewModel.uiState.value.categoryToDelete)
        assertEquals(1, fakeItemRepository.reassignedCategories.size)
        assertEquals(1 to 2, fakeItemRepository.reassignedCategories.first())
        assertEquals(1, fakeRepository.deletedCategories.size)
    }

    @Test
    fun test_dismissReassignDialog_clearsAllReassignState() = runTest(testDispatcher) {
        // Given – set up reassign dialog state
        fakeItemRepository.addItem(
            ItemEntity(
                id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
                unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
                locationId = 1, notes = "", lastUpdated = 0L
            )
        )
        viewModel.showDeleteDialog(CategoryEntity(id = 1, name = "Lebensmittel"))
        advanceUntilIdle()
        viewModel.updateReassignTarget(2)

        // When
        viewModel.dismissReassignDialog()

        // Then
        val state = viewModel.uiState.value
        assertFalse(state.isReassignDialogVisible)
        assertNull(state.categoryToDelete)
        assertEquals(0, state.itemCountForDelete)
        assertNull(state.reassignTargetCategoryId)
    }
}

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

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

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

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

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

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

private class FakeItemRepositoryForCategories : ItemRepository {
    private val _items = mutableListOf<ItemEntity>()
    val reassignedCategories = mutableListOf<Pair<Int, Int>>()

    fun addItem(item: ItemEntity) { _items.add(item) }

    override fun getAll(): Flow<List<ItemEntity>> = MutableStateFlow(_items.toList())
    override suspend fun getById(id: String): ItemEntity? = _items.find { it.id == id }
    override suspend fun insert(item: ItemEntity) { _items.add(item) }
    override suspend fun update(item: ItemEntity) { _items.replaceAll { if (it.id == item.id) item else it } }
    override suspend fun delete(item: ItemEntity) { _items.remove(item) }
    override fun getByCategory(categoryId: Int): Flow<List<ItemEntity>> =
        MutableStateFlow(_items.filter { it.categoryId == categoryId })
    override fun getByLocation(locationId: Int): Flow<List<ItemEntity>> =
        MutableStateFlow(_items.filter { it.locationId == locationId })
    override fun getExpiringSoon(daysUntil: Int): Flow<List<ItemEntity>> = MutableStateFlow(emptyList())
    override suspend fun countByCategoryId(categoryId: Int): Int = _items.count { it.categoryId == categoryId }
    override suspend fun reassignCategory(fromId: Int, toId: Int) {
        reassignedCategories.add(fromId to toId)
        _items.replaceAll { if (it.categoryId == fromId) it.copy(categoryId = toId) else it }
    }
    override suspend fun countByLocationId(locationId: Int): Int = _items.count { it.locationId == locationId }
    override suspend fun reassignLocation(fromId: Int, toId: Int) {}
    override suspend fun getLastUsedLocationId(): Int? = _items.maxByOrNull { it.lastUpdated }?.locationId
}
