package de.krisenvorrat.app.data.repository

import de.krisenvorrat.app.data.db.dao.MessageDao
import de.krisenvorrat.app.data.db.entity.MessageEntity
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.repository.SettingsRepository
import de.krisenvorrat.shared.model.MessageDto
import de.krisenvorrat.shared.model.UserListItemDto
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.client.engine.mock.respondError
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.headersOf
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.cancel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

private class FakeMessageDao : MessageDao {
    val upserted = mutableListOf<MessageEntity>()
    val delivered = mutableListOf<String>()

    override suspend fun upsert(message: MessageEntity) { upserted.add(message) }
    override suspend fun insertIfNotExists(message: MessageEntity) { upserted.add(message) }
    override fun getConversation(myId: String, otherId: String): Flow<List<MessageEntity>> =
        MutableStateFlow(upserted.filter {
            (it.senderId == myId && it.receiverId == otherId) ||
                (it.senderId == otherId && it.receiverId == myId)
        })
    override suspend fun getPendingMessages(): List<MessageEntity> =
        upserted.filter { it.isPending }
    override suspend fun markDelivered(id: String) {
        delivered.add(id)
        val idx = upserted.indexOfFirst { it.id == id }
        if (idx >= 0) upserted[idx] = upserted[idx].copy(isPending = false)
    }
}

private class FakeMessageSettingsRepository : 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<de.krisenvorrat.app.data.db.entity.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 FakeMessageWsClient : WebSocketClient {
    val events2 = MutableSharedFlow<WebSocketEvent>(extraBufferCapacity = 16)
    override val events: SharedFlow<WebSocketEvent> = events2.asSharedFlow()
    override fun connect(serverUrl: String, accessToken: String) = Unit
    override fun disconnect() = Unit
}

@OptIn(ExperimentalCoroutinesApi::class)
class MessageRepositoryImplTest {

    private val jsonSerializer = Json { ignoreUnknownKeys = true; encodeDefaults = true }
    private val testScope = TestScope()

    @After
    fun tearDown() {
        testScope.cancel()
    }

    private fun createClient(engine: MockEngine): HttpClient = HttpClient(engine) {
        install(ContentNegotiation) { json(jsonSerializer) }
    }

    private fun buildRepository(
        dao: FakeMessageDao,
        httpClient: HttpClient,
        settings: FakeMessageSettingsRepository,
        wsClient: FakeMessageWsClient = FakeMessageWsClient()
    ) = MessageRepositoryImpl(
        dao = dao,
        httpClient = httpClient,
        settingsRepository = settings,
        webSocketClient = wsClient,
        scope = testScope
    )

    @Test
    fun test_sendMessage_whenServerReachable_marksDelivered() = runTest {
        // Given
        val dao = FakeMessageDao()
        val settings = FakeMessageSettingsRepository().apply {
            set(SettingsKeys.SERVER_URL, "http://localhost:8080")
            set(SettingsKeys.AUTH_ACCESS_TOKEN, "token")
            set(SettingsKeys.AUTH_USER_ID, "user1")
            set(SettingsKeys.AUTH_USERNAME, "Alice")
        }
        val engine = MockEngine {
            respond(
                content = """{"id":"x","senderId":"user1","senderUsername":"Alice","receiverId":"user2","body":"hi","sentAt":1}""",
                status = HttpStatusCode.Created,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        val repo = buildRepository(dao, createClient(engine), settings)

        // When
        repo.sendMessage("user2", "hi")

        // Then
        assertTrue(dao.delivered.isNotEmpty())
    }

    @Test
    fun test_sendMessage_whenServerUnreachable_remainsPending() = runTest {
        // Given
        val dao = FakeMessageDao()
        val settings = FakeMessageSettingsRepository().apply {
            set(SettingsKeys.SERVER_URL, "http://localhost:8080")
            set(SettingsKeys.AUTH_ACCESS_TOKEN, "token")
            set(SettingsKeys.AUTH_USER_ID, "user1")
            set(SettingsKeys.AUTH_USERNAME, "Alice")
        }
        val engine = MockEngine { respondError(HttpStatusCode.ServiceUnavailable) }
        val repo = buildRepository(dao, createClient(engine), settings)

        // When
        repo.sendMessage("user2", "hi")

        // Then
        assertTrue(dao.delivered.isEmpty())
        assertTrue(dao.upserted.any { it.isPending })
    }

    @Test
    fun test_drainPendingMessages_sendsAllPending() = runTest {
        // Given
        val dao = FakeMessageDao()
        dao.upserted.add(
            MessageEntity("m1", "user1", "Alice", "user2", "hello", 1000L, isPending = true)
        )
        val settings = FakeMessageSettingsRepository().apply {
            set(SettingsKeys.SERVER_URL, "http://localhost:8080")
            set(SettingsKeys.AUTH_ACCESS_TOKEN, "token")
        }
        val engine = MockEngine {
            respond(
                content = """{"id":"m1","senderId":"user1","senderUsername":"Alice","receiverId":"user2","body":"hello","sentAt":1000}""",
                status = HttpStatusCode.Created,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        val repo = buildRepository(dao, createClient(engine), settings)

        // When
        repo.drainPendingMessages()

        // Then
        assertTrue(dao.delivered.contains("m1"))
    }

    @Test
    fun test_fetchUsers_returnsUserList() = runTest {
        // Given
        val settings = FakeMessageSettingsRepository().apply {
            set(SettingsKeys.SERVER_URL, "http://localhost:8080")
            set(SettingsKeys.AUTH_ACCESS_TOKEN, "token")
        }
        val users = listOf(UserListItemDto("u2", "Bob"), UserListItemDto("u3", "Carol"))
        val engine = MockEngine {
            respond(
                content = jsonSerializer.encodeToString(ListSerializer(UserListItemDto.serializer()), users),
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        val repo = buildRepository(FakeMessageDao(), createClient(engine), settings)

        // When
        val result = repo.fetchUsers()

        // Then
        assertTrue(result.isSuccess)
        assertEquals(2, result.getOrNull()?.size)
    }

    @Test
    fun test_newMessageEvent_upsertedToDatabase() = runTest {
        // Given
        val dao = FakeMessageDao()
        val wsClient = FakeMessageWsClient()
        buildRepository(
            dao = dao,
            httpClient = createClient(MockEngine { respondError(HttpStatusCode.ServiceUnavailable) }),
            settings = FakeMessageSettingsRepository(),
            wsClient = wsClient
        )
        // Let init block start and subscribe before emitting
        testScope.advanceUntilIdle()

        val msg = MessageDto("m1", "u2", "Bob", "u1", "hey", 1000L)

        // When
        wsClient.events2.emit(WebSocketEvent.NewMessage(msg))
        testScope.advanceUntilIdle()

        // Then
        assertTrue(dao.upserted.any { it.id == "m1" && !it.isPending })
    }
}

