package net.buzzert.kordophone.backend

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.buzzert.kordophone.backend.db.CachedChatDatabase
import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.model.OutgoingMessage
import net.buzzert.kordophone.backend.server.APIClient
import net.buzzert.kordophone.backend.server.APIClientFactory
import net.buzzert.kordophone.backend.server.APIInterface
import net.buzzert.kordophone.backend.server.Authentication
import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophone.backend.server.RetrofitAPIClient
import net.buzzert.kordophone.backend.server.UpdateMonitor
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.net.URL
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class BackendTests {
    private fun liveRepository(host: String): Pair<ChatRepository, RetrofitAPIClient> {
        val client = RetrofitAPIClient(URL(host), authentication = Authentication("test", "test"))
        val database = CachedChatDatabase.testDatabase()
        val repository = ChatRepository(client, database)
        return Pair(repository, client)
    }

    private fun mockRepository(): Pair<ChatRepository, MockServer> {
        val mockServer = MockServer()
        val database = CachedChatDatabase.testDatabase()
        val repository = ChatRepository(mockServer.getClient(), database)
        return Pair(repository, mockServer)
    }

    @Test
    fun testCreateClientAcceptsBaseUrlWithoutTrailingSlash() {
        val client = APIClientFactory.createClient(
            "https://example.com/api",
            Authentication("test", "test")
        )

        assertTrue(client.isConfigured)
    }

    @Test
    fun testGetVersion() = runBlocking {
        val (repository, mockServer) = mockRepository()
        val version = repository.getVersion()
        assertEquals(version, mockServer.version)

        repository.close()
    }

    @Test
    fun testFetchConversations() = runBlocking {
        val (repository, mockServer) = mockRepository()

        // Add conversation to mock server
        val inConversation = mockServer.addTestConversations(1).first()

        val conversations = repository.testingHarness().fetchConversations()
        assertEquals(conversations.count(), 1)

        val outConversation = conversations.first()
        assertEquals(inConversation, outConversation)

        repository.close()
    }

    @Test
    fun testFetchMessages() = runBlocking {
        val (repository, mockServer) = mockRepository()

        // Add conversation & message to mock server
        val inConversation = mockServer.addTestConversations(1).first()
        val inMessage = mockServer.addTestMessages(1, inConversation).first()

        val conversations = repository.testingHarness().fetchConversations()
        val messages = repository.testingHarness().fetchMessages(conversations.first())
        assertEquals(messages.count(), 1)

        val outMessage = messages.first()
        assertEquals(outMessage, inMessage)

        repository.close()
    }

    @Test
    fun testSendMessage() = runBlocking {
        val (repository, mockServer) = mockRepository()

        val conversation = mockServer.addTestConversations(1).first()
        val generatedMessage = MockServer.generateMessage(conversation)
        val outgoingMessage = OutgoingMessage(
            body = generatedMessage.text,
            conversation = conversation,
            attachmentUris = setOf(),
            attachmentDataSource = { null },
        )

        repository.enqueueOutgoingMessage(outgoingMessage)

        val event = repository.messageDeliveredChannel.first()
        assertEquals(event.message.text, outgoingMessage.body)

        repository.close()
    }

    @Test
    fun testConversationSynchronization() = runBlocking {
        val (repo, mockServer) = mockRepository()

        // Add some test convos
        val conversations = mockServer.addTestConversations(10)

        // Sync
        repo.synchronize()

        // Check our count.
        assertEquals(10, repo.conversations.count())

        // Sync again: let's ensure we're de-duplicating conversations.
        repo.synchronize()

        // Should be no change...
        assertEquals(10, repo.conversations.count())

        // Say unread count + lastMessage preview changes on server.
        val someConversation = conversations.first().apply {
            displayName = "COOL"
            unreadCount = 2
        }

        // Sync again
        repo.synchronize()

        // Make sure change is reflected
        val readConversation = repo.conversationForGuid(someConversation.guid)
        assertEquals("COOL", readConversation.displayName)
        assertEquals(2, readConversation.unreadCount)

        repo.close()
    }

    @Test
    fun testConversationFlowUpdates() = runBlocking {
        val (repo, mockServer) = mockRepository()

        // Set up flow watcher, asynchronously
        val updateLatch = CountDownLatch(1)
        val job = launch {
            println("Watching for conversations changes...")
            repo.conversationChanges.collect {
                println("Changed conversations: $it")

                // We got it.
                if (it.isNotEmpty()) {
                    println("bink")
                    updateLatch.countDown()
                    cancel()
                }
            }
        }

        withContext(Dispatchers.IO) {
            // Add a conversation
            println("Adding conversation")
            mockServer.addTestConversations(1)

            // Sync. This should trigger an update
            println("Synchronizing...")
            repo.synchronize()

            // Wait for the coroutine that is collecting the flow to finish
            job.join()

            // Ensure the updates have been processed before proceeding
            assertTrue(updateLatch.await(1, TimeUnit.SECONDS))
        }
    }

    @Test
    fun testMessageFlowUpdates() = runBlocking {
        val (repo, mockServer) = mockRepository()

        // Add an existing conversation
        println("Adding conversation")
        val conversation = mockServer.addTestConversations(1).first()

        // Initial sync
        println("Initial sync")
        repo.synchronize()

        // Set up flow watcher, asynchronously
        var messagesAdded: List<Message>? = null
        val updateLatch = CountDownLatch(1)
        val job = launch {
            println("Watching for messages to be added...")
            repo.messagesChanged(conversation).collect {
                println("Messages changed: $it")

                if (it.isNotEmpty()) {
                    messagesAdded = it
                    updateLatch.countDown()
                    cancel()
                }
            }
        }

        withContext(Dispatchers.IO) {
            // Add a message
            val messages = mockServer.addTestMessages(10, conversation)

            // Sync. This should trigger an update
            println("Synchronizing...")
            repo.synchronize()

            // Wait for the coroutine that is collecting the flow to finish
            job.join()

            // Ensure the updates have been processed before proceeding
            assertTrue(updateLatch.await(1, TimeUnit.SECONDS))

            // Check what we got back
            assertEquals(messages, messagesAdded)
        }
    }

    @Test
    fun testUpdateMonitorForConversations() = runBlocking {
        val mockServer = MockServer()
        val mockAPIClient = mockServer.getClient()
        val updateMonitor = UpdateMonitor(mockAPIClient)

        // Set up flow watcher, asynchronously
        val updateLatch = CountDownLatch(1)
        val job = launch {
            updateMonitor.beginMonitoringUpdates()
            updateMonitor.conversationChanged.collect {
                println("Got conversation changed: $it")
                updateLatch.countDown()

                updateMonitor.stopMonitoringForUpdates()
                mockAPIClient.stopWatchingForUpdates()
                cancel()
            }
        }

        withContext(Dispatchers.IO) {
            Thread.sleep(500)

            // Add a conversation
            println("Adding conversation")
            mockServer.addTestConversations(1)

            // Wait for the coroutine that is collecting the flow to finish
            job.join()

            // Ensure the updates have been processed before proceeding
            assertTrue(updateLatch.await(1, TimeUnit.SECONDS))
        }
    }

    @Test
    fun testUpdateMonitorForMessages() = runBlocking {
        val mockServer = MockServer()
        val mockAPIClient = mockServer.getClient()
        val updateMonitor = UpdateMonitor(mockAPIClient)

        // Set up flow watcher, asynchronously
        val updateLatch = CountDownLatch(1)
        val job = launch {
            updateMonitor.beginMonitoringUpdates()
            updateMonitor.messageAdded.collect {
                println("Got message added: $it")
                updateLatch.countDown()

                updateMonitor.stopMonitoringForUpdates()
                mockAPIClient.stopWatchingForUpdates()
                cancel()
            }
        }

        withContext(Dispatchers.IO) {
            Thread.sleep(500)

            // Add a conversation
            println("Adding conversation")
            val convo = mockServer.addTestConversations(1).first()

            // Add a test message
            mockServer.addTestMessages(1, convo)

            // Wait for the coroutine that is collecting the flow to finish
            job.join()

            // Ensure the updates have been processed before proceeding
            assertTrue(updateLatch.await(1, TimeUnit.SECONDS))
        }
    }

    @Test
    fun testEndToEndMessageUpdates() = runBlocking {
        val (repo, mockServer) = mockRepository()

        val conversation = mockServer.addTestConversations(1).first()

        // Initial sync
        repo.synchronize()

        // We're going to generate a couple of messages...
        val messagesToGenerate = 5

        // Start watching for N updates
        val updateLatch = CountDownLatch(messagesToGenerate)
        val monitorJob = launch {
            repo.messagesChanged(conversation).collect {
                println("Message changed: $it")

                if (it.isNotEmpty()) {
                    updateLatch.countDown()
                }

                if (updateLatch.count == 0L) {
                    repo.stopWatchingForUpdates()
                    cancel()
                }
            }
        }

        withContext(Dispatchers.IO) {
            repo.beginWatchingForUpdates(this)

            Thread.sleep(500)

            // Should trigger an update
            println("Adding messages")
            mockServer.addTestMessages(messagesToGenerate, conversation)

            monitorJob.join()

            assertTrue(updateLatch.await(1, TimeUnit.SECONDS))

            // Check num messages
            val allMessages = repo.messagesForConversation(conversation)
            assertEquals(messagesToGenerate, allMessages.count())
        }
    }
}
