package de.krisenvorrat.app.ui.dashboard

import de.krisenvorrat.app.data.db.entity.CategoryEntity
import de.krisenvorrat.app.data.db.entity.ItemEntity
import de.krisenvorrat.app.data.db.entity.SettingsEntity
import de.krisenvorrat.app.domain.model.AgeGroup
import de.krisenvorrat.app.domain.model.SettingsKey
import de.krisenvorrat.app.domain.model.defaultAgeGroups
import de.krisenvorrat.app.domain.model.toJson
import de.krisenvorrat.app.domain.repository.CategoryRepository
import de.krisenvorrat.app.domain.repository.ItemRepository
import de.krisenvorrat.app.domain.repository.SettingsRepository
import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
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.assertTrue
import org.junit.Before
import org.junit.Test
import java.time.LocalDate

@OptIn(ExperimentalCoroutinesApi::class)
class DashboardViewModelTest {

    private val testDispatcher = StandardTestDispatcher()
    private lateinit var fakeItemRepository: FakeItemRepository
    private lateinit var fakeCategoryRepository: FakeCategoryRepository
    private lateinit var fakeSettingsRepository: FakeSettingsRepository
    private lateinit var viewModel: DashboardViewModel

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
        fakeItemRepository = FakeItemRepository()
        fakeCategoryRepository = FakeCategoryRepository()
        fakeSettingsRepository = FakeSettingsRepository()
    }

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

    private fun createViewModel() = DashboardViewModel(
        itemRepository = fakeItemRepository,
        categoryRepository = fakeCategoryRepository,
        settingsRepository = fakeSettingsRepository,
        calculateCategorySummary = CalculateCategorySummaryUseCase(),
        calculateTotalValue = CalculateTotalValueUseCase(),
        calculateSupplyRange = CalculateSupplyRangeUseCase(),
        getExpiryWarnings = GetExpiryWarningsUseCase()
    )

    @Test
    fun test_init_withNoData_stateHasEmptyLists() = runTest(testDispatcher) {
        // Given – empty repositories
        viewModel = createViewModel()

        // When
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertFalse(state.isLoading)
        assertTrue(state.categorySummaries.isEmpty())
        assertEquals(0.0, state.totalValue, 0.001)
        assertEquals(0.0, state.supplyRangeDays, 0.001)
        assertFalse(state.hasExpiryWarnings)
        assertEquals(0, state.totalItemCount)
    }

    @Test
    fun test_init_withItems_categorySummariesAreCalculated() = runTest(testDispatcher) {
        // Given
        fakeCategoryRepository.emit(
            listOf(
                CategoryEntity(id = 1, name = "Lebensmittel"),
                CategoryEntity(id = 2, name = "Hygiene")
            )
        )
        fakeItemRepository.emit(
            listOf(
                buildTestItem(id = "a", categoryId = 1, quantity = 2.0, unitPrice = 3.0),
                buildTestItem(id = "b", categoryId = 1, quantity = 1.0, unitPrice = 5.0),
                buildTestItem(id = "c", categoryId = 2, quantity = 4.0, unitPrice = 2.0)
            )
        )
        viewModel = createViewModel()

        // When
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertEquals(2, state.categorySummaries.size)
        assertEquals(3, state.totalItemCount)

        val hygiene = state.categorySummaries.first { it.categoryName == "Hygiene" }
        assertEquals(1, hygiene.itemCount)
        assertEquals(8.0, hygiene.totalValue, 0.001)

        val lebensmittel = state.categorySummaries.first { it.categoryName == "Lebensmittel" }
        assertEquals(2, lebensmittel.itemCount)
        assertEquals(11.0, lebensmittel.totalValue, 0.001)
    }

    @Test
    fun test_init_withItems_totalValueIsCalculated() = runTest(testDispatcher) {
        // Given
        fakeItemRepository.emit(
            listOf(
                buildTestItem(id = "a", quantity = 2.0, unitPrice = 3.0),
                buildTestItem(id = "b", quantity = 1.0, unitPrice = 5.0)
            )
        )
        viewModel = createViewModel()

        // When
        advanceUntilIdle()

        // Then
        assertEquals(11.0, viewModel.uiState.value.totalValue, 0.001)
    }

    @Test
    fun test_init_withKcalItems_supplyRangeIsCalculated() = runTest(testDispatcher) {
        // Given – 1kg item with 200 kcal/100g = 2000 kcal total
        // AgeGroups: ERWACHSENER count=2, kcal=2000 → 4000 kcal/day → 0.5 days
        val ageGroups = defaultAgeGroups().map { entry ->
            if (entry.ageGroup == AgeGroup.ERWACHSENER) entry.copy(count = 2, kcalPerDay = 2000)
            else entry
        }
        fakeSettingsRepository.setValue(
            SettingsKey.StringKey.AgeGroups.key,
            ageGroups.toJson()
        )
        fakeItemRepository.emit(
            listOf(
                buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 2000)
            )
        )
        viewModel = createViewModel()

        // When
        advanceUntilIdle()

        // Then
        assertEquals(0.5, viewModel.uiState.value.supplyRangeDays, 0.001)
    }

    @Test
    fun test_init_withEmptyAgeGroups_usesDefaultTotalKcalForRange() = runTest(testDispatcher) {
        // Given – 1000g × 400 kcal/100g = 4000 kcal; alle AgeGroup-counts = 0
        // → totalDailyKcal = 0 → Fallback auf DEFAULT_TOTAL_DAILY_KCAL = 4000
        // → supplyRangeDays = 4000 / 4000 = 1.0
        val allZeroAgeGroups = defaultAgeGroups().map { it.copy(count = 0) }
        fakeSettingsRepository.setValue(
            SettingsKey.StringKey.AgeGroups.key,
            allZeroAgeGroups.toJson()
        )
        fakeItemRepository.emit(
            listOf(buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 4000))
        )
        viewModel = createViewModel()

        // When
        advanceUntilIdle()

        // Then
        assertEquals(1.0, viewModel.uiState.value.supplyRangeDays, 0.001)
    }

    @Test
    fun test_init_withExpiringItems_expiryWarningsArePresent() = runTest(testDispatcher) {
        // Given – item expiring in 3 months (within URGENT_MONTHS=6)
        val expiryDate = LocalDate.now().plusMonths(3)
        fakeItemRepository.emit(
            listOf(buildTestItem(id = "a", expiryDate = expiryDate))
        )
        viewModel = createViewModel()

        // When
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertTrue(state.hasExpiryWarnings)
        assertEquals(1, state.expiryWarnings.size)
    }

    @Test
    fun test_init_withFarExpiryDate_noExpiryWarnings() = runTest(testDispatcher) {
        // Given – item expiring in 2 years (beyond WARNING_MONTHS=12)
        val expiryDate = LocalDate.now().plusYears(2)
        fakeItemRepository.emit(
            listOf(buildTestItem(id = "a", expiryDate = expiryDate))
        )
        viewModel = createViewModel()

        // When
        advanceUntilIdle()

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

    @Test
    fun test_reactiveUpdate_whenItemsChange_stateUpdates() = runTest(testDispatcher) {
        // Given
        viewModel = createViewModel()
        advanceUntilIdle()
        assertEquals(0, viewModel.uiState.value.totalItemCount)

        // When – items appear
        fakeCategoryRepository.emit(listOf(CategoryEntity(id = 1, name = "Test")))
        fakeItemRepository.emit(
            listOf(buildTestItem(id = "a", categoryId = 1, quantity = 2.0, unitPrice = 5.0))
        )
        advanceUntilIdle()

        // Then
        val state = viewModel.uiState.value
        assertEquals(1, state.totalItemCount)
        assertEquals(10.0, state.totalValue, 0.001)
    }
}

// region Test Helpers

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

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

    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) {
        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 FakeSettingsRepository : SettingsRepository {
    private val store = mutableMapOf<String, String>()
    private val observeFlows = mutableMapOf<String, MutableStateFlow<String?>>()
    private val allFlow = MutableStateFlow<List<SettingsEntity>>(emptyList())

    override suspend fun getValue(key: String): String? = store[key]

    override suspend fun setValue(key: String, value: String) {
        store[key] = value
        observeFlows[key]?.value = value
        allFlow.value = store.map { SettingsEntity(key = it.key, value = it.value) }
    }

    override fun observeValue(key: String): Flow<String?> =
        observeFlows.getOrPut(key) { MutableStateFlow(store[key]) }

    override fun getAll(): Flow<List<SettingsEntity>> = allFlow

    override suspend fun getString(key: SettingsKey.StringKey): String =
        getValue(key.key) ?: key.defaultValue

    override suspend fun getStringOrNull(key: SettingsKey.StringKey): String? =
        getValue(key.key)

    override suspend fun setString(key: SettingsKey.StringKey, value: String) =
        setValue(key.key, value)

    override fun observeString(key: SettingsKey.StringKey): Flow<String> {
        val flow = observeFlows.getOrPut(key.key) { MutableStateFlow(store[key.key]) }
        return flow.map { it ?: key.defaultValue }
    }
}

// endregion
