package de.krisenvorrat.app.data.repository

import de.krisenvorrat.app.data.db.dao.ItemDao
import de.krisenvorrat.app.data.db.dao.PendingSyncOpDao
import de.krisenvorrat.app.data.db.entity.ItemEntity
import de.krisenvorrat.app.data.db.entity.PendingSyncOpEntity
import de.krisenvorrat.app.data.db.entity.SettingsEntity
import de.krisenvorrat.app.data.sync.WebSocketClient
import de.krisenvorrat.app.data.sync.WebSocketEvent
import de.krisenvorrat.app.domain.model.SettingsKey
import de.krisenvorrat.app.domain.model.SettingsKeys
import de.krisenvorrat.app.domain.model.SyncError
import de.krisenvorrat.app.domain.repository.SettingsRepository
import de.krisenvorrat.app.domain.repository.SyncService
import de.krisenvorrat.shared.model.InventoryDto
import de.krisenvorrat.shared.model.ItemDto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate

private class FakeItemDao : ItemDao {
    private val items = mutableListOf<ItemEntity>()
    private val flow = MutableStateFlow<List<ItemEntity>>(emptyList())
    private val expiringSoonFlow = MutableStateFlow<List<ItemEntity>>(emptyList())
    fun setExpiringSoonItems(items: List<ItemEntity>) { expiringSoonFlow.value = items }

    private fun emit() { flow.value = items.toList() }

    override suspend fun insert(item: ItemEntity) {
        items.add(item)
        emit()
    }

    override suspend fun update(item: ItemEntity) {
        val idx = items.indexOfFirst { it.id == item.id }
        if (idx >= 0) {
            items[idx] = item
            emit()
        }
    }

    override suspend fun delete(item: ItemEntity) {
        items.remove(item)
        emit()
    }

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

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

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

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

    override fun getExpiringSoonByCutoff(cutoff: LocalDate): Flow<List<ItemEntity>> =
        expiringSoonFlow

    override suspend fun upsertAll(items: List<ItemEntity>) {
        items.forEach { item ->
            val idx = this.items.indexOfFirst { it.id == item.id }
            if (idx >= 0) this.items[idx] = item else this.items.add(item)
        }
        emit()
    }

    override suspend fun countByCategoryId(categoryId: Int): Int =
        items.count { it.categoryId == categoryId }

    override suspend fun updateCategoryId(fromId: Int, toId: Int) {
        items.replaceAll { if (it.categoryId == fromId) it.copy(categoryId = toId) else it }
        emit()
    }

    override suspend fun countByLocationId(locationId: Int): Int =
        items.count { it.locationId == locationId }

    override suspend fun updateLocationId(fromId: Int, toId: Int) {
        items.replaceAll { if (it.locationId == fromId) it.copy(locationId = toId) else it }
        emit()
    }

    override suspend fun getLastUsedLocationId(): Int? =
        items.maxByOrNull { it.lastUpdated }?.locationId

    override suspend fun deleteByIds(ids: List<String>) {
        items.removeAll { it.id in ids }
        emit()
    }
}

private class FakePendingSyncOpDao : PendingSyncOpDao {
    val ops = mutableListOf<PendingSyncOpEntity>()

    override suspend fun insert(op: PendingSyncOpEntity) { ops.add(op) }
    override suspend fun getAll(): List<PendingSyncOpEntity> = ops.toList()
    override suspend fun deleteById(id: String) { ops.removeAll { it.id == id } }
    override suspend fun deleteByItemId(itemId: String) { ops.removeAll { it.itemId == itemId } }
}

private class FakeSettingsRepository : SettingsRepository {
    private val store = mutableMapOf<String, String>()
    fun set(key: String, value: String) { store[key] = value }

    override suspend fun getValue(key: String): String? = store[key]
    override suspend fun setValue(key: String, value: String) { store[key] = value }
    override fun observeValue(key: String): Flow<String?> = MutableStateFlow(store[key])
    override fun getAll(): Flow<List<SettingsEntity>> = MutableStateFlow(emptyList())

    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> =
        MutableStateFlow(store[key.key] ?: key.defaultValue)
}

private class FakeSyncService : SyncService {
    var patchResult: Result<Unit> = Result.success(Unit)
    var deleteResult: Result<Unit> = Result.success(Unit)
    val patchedItems = mutableListOf<Pair<String, ItemDto>>()
    val deletedItemIds = mutableListOf<String>()

    override suspend fun patchItem(itemId: String, item: ItemDto): Result<Unit> {
        patchedItems.add(itemId to item)
        return patchResult
    }

    override suspend fun deleteItem(itemId: String): Result<Unit> {
        deletedItemIds.add(itemId)
        return deleteResult
    }

    override suspend fun downloadInventory(since: Long?): Result<InventoryDto> = Result.success(
        InventoryDto(categories = emptyList(), locations = emptyList(), items = emptyList(), settings = emptyList())
    )
    override suspend fun uploadInventory(inventory: InventoryDto): Result<InventoryDto> =
        Result.success(inventory)
    override suspend fun login(serverUrl: String, username: String, password: String): Result<Unit> =
        Result.success(Unit)
    override suspend fun logout() {}
    override suspend fun listInventories(): Result<List<de.krisenvorrat.shared.model.InventoryInfoDto>> = Result.success(emptyList())
    override suspend fun createInventory(name: String): Result<de.krisenvorrat.shared.model.InventoryInfoDto> =
        Result.success(de.krisenvorrat.shared.model.InventoryInfoDto(id = "new-inv", name = name, isActive = true))
    override suspend fun switchInventory(inventoryId: String): Result<de.krisenvorrat.shared.model.InventoryInfoDto> =
        Result.success(de.krisenvorrat.shared.model.InventoryInfoDto(id = inventoryId, name = "Switched", isActive = true))
}

private class FakeWebSocketClient : WebSocketClient {
    private val _events = MutableSharedFlow<WebSocketEvent>(extraBufferCapacity = 10)
    override val events: SharedFlow<WebSocketEvent> = _events
    suspend fun emit(event: WebSocketEvent) { _events.emit(event) }
    override fun connect(serverUrl: String, accessToken: String) {}
    override fun disconnect() {}
}

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

@OptIn(ExperimentalCoroutinesApi::class)
class ItemRepositoryImplTest {

    private val fakeDao = FakeItemDao()
    private val fakePendingSyncOpDao = FakePendingSyncOpDao()
    private val fakeSettingsRepository = FakeSettingsRepository()
    private val fakeSyncService = FakeSyncService()
    private val fakeWebSocketClient = FakeWebSocketClient()
    private val testScope = TestScope()

    private fun buildRepository(scope: CoroutineScope = testScope) = ItemRepositoryImpl(
        dao = fakeDao,
        syncService = fakeSyncService,
        pendingSyncOpDao = fakePendingSyncOpDao,
        settingsRepository = fakeSettingsRepository,
        webSocketClient = fakeWebSocketClient,
        scope = scope
    )

    @Test
    fun test_insert_withNewItem_itemAppearsInGetAll() = runBlocking {
        // Given
        val repository = buildRepository()
        val item = buildItem(id = "abc")

        // When
        repository.insert(item)

        // Then
        val result = repository.getAll().first()
        assertTrue(result.any { it.id == "abc" })
    }

    @Test
    fun test_getById_withExistingId_returnsItem() = runBlocking {
        // Given
        val repository = buildRepository()
        val item = buildItem(id = "abc")
        repository.insert(item)

        // When
        val result = repository.getById("abc")

        // Then
        assertEquals(item, result)
    }

    @Test
    fun test_getById_withUnknownId_returnsNull() = runBlocking {
        // Given / When
        val result = buildRepository().getById("unknown")

        // Then
        assertNull(result)
    }

    @Test
    fun test_update_withExistingItem_itemIsUpdated() = runBlocking {
        // Given
        val repository = buildRepository()
        val item = buildItem(id = "abc")
        repository.insert(item)
        val updated = item.copy(name = "Aktualisiert")

        // When
        repository.update(updated)

        // Then
        val result = repository.getById("abc")
        assertEquals("Aktualisiert", result?.name)
    }

    @Test
    fun test_delete_withExistingItem_itemRemovedFromGetAll() = runBlocking {
        // Given
        val repository = buildRepository()
        val item = buildItem(id = "abc")
        repository.insert(item)

        // When
        repository.delete(item)

        // Then
        val result = repository.getAll().first()
        assertFalse(result.any { it.id == "abc" })
    }

    @Test
    fun test_getByCategory_withMatchingItems_returnsFilteredItems() = runBlocking {
        // Given
        val repository = buildRepository()
        repository.insert(buildItem(id = "a", categoryId = 1))
        repository.insert(buildItem(id = "b", categoryId = 2))

        // When
        val result = repository.getByCategory(1).first()

        // Then
        assertEquals(1, result.size)
        assertEquals("a", result.first().id)
    }

    @Test
    fun test_getByLocation_withMatchingItems_returnsFilteredItems() = runBlocking {
        // Given
        val repository = buildRepository()
        repository.insert(buildItem(id = "a", locationId = 1))
        repository.insert(buildItem(id = "b", locationId = 2))

        // When
        val result = repository.getByLocation(2).first()

        // Then
        assertEquals(1, result.size)
        assertEquals("b", result.first().id)
    }

    @Test
    fun test_getExpiringSoon_withPreconfiguredFlow_returnsExpectedItems() = runBlocking {
        // Given
        val repository = buildRepository()
        val expiring = buildItem(id = "exp")
        fakeDao.setExpiringSoonItems(listOf(expiring))

        // When
        val result = repository.getExpiringSoon(30).first()

        // Then
        assertEquals(1, result.size)
        assertEquals("exp", result.first().id)
    }

    @Test
    fun test_insert_whenServerConfigured_callsPatchItem() = runBlocking {
        // Given
        fakeSettingsRepository.set(SettingsKeys.AUTH_ACCESS_TOKEN, "token123")
        val syncScope = CoroutineScope(Dispatchers.Unconfined)
        val repository = buildRepository(scope = syncScope)
        val item = buildItem(id = "sync1")

        // When
        repository.insert(item)

        // Then
        assertTrue(fakeSyncService.patchedItems.any { it.first == "sync1" })
        syncScope.cancel()
    }

    @Test
    fun test_insert_whenPatchFails_queuesOp() = runBlocking {
        // Given
        fakeSettingsRepository.set(SettingsKeys.AUTH_ACCESS_TOKEN, "token123")
        fakeSyncService.patchResult = Result.failure(SyncError.ConnectionError())
        val syncScope = CoroutineScope(Dispatchers.Unconfined)
        val repository = buildRepository(scope = syncScope)
        val item = buildItem(id = "queue1")

        // When
        repository.insert(item)

        // Then
        val ops = fakePendingSyncOpDao.getAll()
        assertTrue(ops.any { it.itemId == "queue1" && it.operation == "PATCH" })
        syncScope.cancel()
    }

    @Test
    fun test_insert_whenNotConfigured_doesNotQueue() = runBlocking {
        // Given – kein Token gesetzt
        val repository = buildRepository()
        val item = buildItem(id = "noconf")

        // When
        repository.insert(item)

        // Then
        assertTrue(fakePendingSyncOpDao.ops.isEmpty())
        assertTrue(fakeSyncService.patchedItems.isEmpty())
    }

    @Test
    fun test_delete_queuesDeleteOp_onNetworkError() = runBlocking {
        // Given
        fakeSettingsRepository.set(SettingsKeys.AUTH_ACCESS_TOKEN, "token123")
        fakeSyncService.deleteResult = Result.failure(SyncError.ConnectionError())
        val syncScope = CoroutineScope(Dispatchers.Unconfined)
        val repository = buildRepository(scope = syncScope)
        val item = buildItem(id = "del1")
        fakeDao.insert(item)

        // When
        repository.delete(item)

        // Then
        val ops = fakePendingSyncOpDao.getAll()
        assertTrue(ops.any { it.itemId == "del1" && it.operation == "DELETE" })
        syncScope.cancel()
    }

    @Test
    fun test_drainQueue_onConnectedEvent_executesAndClearsOps() =
        runTest(UnconfinedTestDispatcher()) {
            // Given
            fakeSettingsRepository.set(SettingsKeys.AUTH_ACCESS_TOKEN, "token123")
            fakeSyncService.patchResult = Result.success(Unit)
            val drainScope = CoroutineScope(UnconfinedTestDispatcher())
            val repository = buildRepository(scope = drainScope)
            fakePendingSyncOpDao.insert(
                PendingSyncOpEntity(
                    id = "op1",
                    itemId = "drain1",
                    operation = "PATCH",
                    payload = """{"id":"drain1","name":"Konserve","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerKg":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":0}""",
                    createdAt = 1000L
                )
            )

            // When
            fakeWebSocketClient.emit(WebSocketEvent.Connected)

            // Then
            assertTrue(fakePendingSyncOpDao.ops.isEmpty())
            assertTrue(fakeSyncService.patchedItems.any { it.first == "drain1" })
            drainScope.cancel()
        }
}

