diff --git a/src/main/kotlin/chatroom/sendmessages/SendMessagePresenter.kt b/src/main/kotlin/chatroom/sendmessages/SendMessagePresenter.kt index a726a93..80942e7 100644 --- a/src/main/kotlin/chatroom/sendmessages/SendMessagePresenter.kt +++ b/src/main/kotlin/chatroom/sendmessages/SendMessagePresenter.kt @@ -4,14 +4,13 @@ import arch.Presenter import arch.RokyDispatchers import chatroom.sendmessages.SendMessageEvent.SendMessage import chatroom.sendmessages.SendMessageViewState.Clear -import chatserver.MessagesRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class SendMessagePresenter( private val windowScope: CoroutineScope, - private val send: MessagesRepository.Write, + private val send: (String) -> Unit = ::println, dispatchers: RokyDispatchers, ) : Presenter(dispatchers) { override fun onAttach(view: SendMessagesView) { @@ -25,7 +24,7 @@ class SendMessagePresenter( fun onEvent(event: SendMessageEvent) { if (event is SendMessage) { windowScope.launch(dispatchers.io) { - send.send(event.message) + send(event.message) withContext(dispatchers.main) { withView { it.show(Clear) } } diff --git a/src/main/kotlin/chatroom/sendmessages/SendMessages.kt b/src/main/kotlin/chatroom/sendmessages/SendMessages.kt index 3ecafa7..8a22ad7 100644 --- a/src/main/kotlin/chatroom/sendmessages/SendMessages.kt +++ b/src/main/kotlin/chatroom/sendmessages/SendMessages.kt @@ -9,7 +9,6 @@ val sendMessagesModule = scoped { SendMessagePresenter( dispatchers = get(), - send = get(), windowScope = get().windowScope, ) } diff --git a/src/main/kotlin/chatroom/users/UsersListUseCase.kt b/src/main/kotlin/chatroom/users/UsersListUseCase.kt index 1c5800d..ea9b00e 100644 --- a/src/main/kotlin/chatroom/users/UsersListUseCase.kt +++ b/src/main/kotlin/chatroom/users/UsersListUseCase.kt @@ -1,6 +1,6 @@ package chatroom.users -import chatroom.viewmessages.ViewMessagesUseCase +import chatserver.messages.LocalChatMessages import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -8,7 +8,7 @@ import kotlin.random.Random import kotlin.time.Duration.Companion.seconds class UsersListUseCase( - private val users: List = ViewMessagesUseCase.sampleUsers, + private val users: List = LocalChatMessages.sampleUsers, private val rndInt: () -> Int = { Random.nextInt(4, users.size - 1) }, private val rndUser: (List) -> String = { it.random() }, ) { diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt index a2f7c5f..e14bb92 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt @@ -1,18 +1,19 @@ package chatroom.viewmessages import chatroom.ChatroomWindow +import chatserver.ChatRepositories import org.koin.dsl.module val viewMessagesModule = module { scope { scoped { ViewMessagesPanel(get()) } - scoped { ViewMessagesUseCase(read = get()) } scoped { ViewMessagesPresenter( windowScope = get().windowScope, dispatchers = get(), - messages = get(), + read = get().readMessages(), + channel = get().subscribeMessages(), ) } } diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt index 91b0a0d..5c5d9e0 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt @@ -4,28 +4,38 @@ import arch.Presenter import arch.RokyDispatchers import chatroom.viewmessages.ViewMessagesViewState.Messages import chatroom.viewmessages.ViewMessagesViewState.NoMessages +import chatserver.ChatMessageResult +import chatserver.ReadChatRepository +import chatserver.SubscribeChatRepository import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ViewMessagesPresenter( private val windowScope: CoroutineScope, - private val messages: ViewMessagesUseCase, + private val read: ReadChatRepository, + private val channel: SubscribeChatRepository, dispatchers: RokyDispatchers, ) : Presenter(dispatchers) { override fun onAttach(view: ViewMessagesView) { view.show(NoMessages) windowScope.launch(dispatchers.io) { - messages().map { Messages(it) }.collect { message -> - withContext(dispatchers.main) { - withView { it.show(message) } + read.observe() + .filter { it.isOk } + .map { it.item } + .map(::Messages) + .collect { message -> + withContext(dispatchers.main) { + withView { it.show(message) } + } } - } } + channel.subscribe() } override fun onDetach(view: ViewMessagesView) { - // deliberately empty to please the auto-formatter + channel.unsubscribe() } } diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessagesUseCase.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessagesUseCase.kt deleted file mode 100644 index d8061ca..0000000 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessagesUseCase.kt +++ /dev/null @@ -1,66 +0,0 @@ -package chatroom.viewmessages - -import chatserver.MessagesRepository -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlin.time.Duration.Companion.seconds - -class ViewMessagesUseCase( - private val messages: List = sampleMessages, - private val users: List = sampleUsers, - private val read: MessagesRepository.Read, -) { - operator fun invoke(): Flow { - val rnd = - flow { - while (true) { - delay(3.seconds) - emit("${users.random()}: ${messages.random()}") - } - } - return merge(rnd, read.observe().map { "Me: $it" }) - } - - companion object { - private val sampleMessages = - listOf( - "This is a coup!", - "What time's Roky Coding tonight?", - "Look at the calendar...", - "Charizard", - "Remind me to get my washing at 4 PM", - "ASMR....", - "BRAIN...praise me", - "Biggleswade is naff", - "Biggleswade is amazing /s", - "I love Biggleswade!!!", - "AHHHHHHHHHHHHHHHHHH", - "Wordle: 4/6", - "Wordle: 1/6", - "Wordle: 2/6", - "I AM SO HUNGRY RN", - "I am feeling quiet today", - ":thumbs_up:", - "I just have a bit of a cold rn", - "I just think it's something going around", - "Don't just type out what I'm saying Mike", - ":breathing_noises:", - ) - val sampleUsers = - listOf( - "Martine", - "Ed", - "Kai", - "Terry", - "Robert", - "Tom", - "Brian", - "Dunia", - "Stefano", - "Mike", - ) - } -} diff --git a/src/main/kotlin/chatserver/ChatMessageResult.kt b/src/main/kotlin/chatserver/ChatMessageResult.kt new file mode 100644 index 0000000..d8b1470 --- /dev/null +++ b/src/main/kotlin/chatserver/ChatMessageResult.kt @@ -0,0 +1,16 @@ +package chatserver + +import chatserver.ReadChatRepository.ReadResult + +data class ChatMessageResult( + override val isOk: Boolean, + override val item: String, + override val error: Exception?, +) : + ReadResult { + companion object { + fun ok(item: String): ChatMessageResult = ChatMessageResult(true, item, null) + + fun fail(e: Exception? = null): ChatMessageResult = ChatMessageResult(false, "", e) + } +} diff --git a/src/main/kotlin/chatserver/ChatRepositories.kt b/src/main/kotlin/chatserver/ChatRepositories.kt index 9ae4cf0..df27581 100644 --- a/src/main/kotlin/chatserver/ChatRepositories.kt +++ b/src/main/kotlin/chatserver/ChatRepositories.kt @@ -1,13 +1,19 @@ package chatserver +import chatserver.messages.LocalChatMessages import chatserver.profiles.LocalProfilesRepository class ChatRepositories( private val profiles: LocalProfilesRepository, + private val messages: LocalChatMessages, ) { fun writeProfiles(): WriteChatRepository = profiles fun readProfiles(): ReadChatRepository = profiles fun subscribeProfiles(): SubscribeChatRepository = profiles + + fun readMessages(): ReadChatRepository = messages + + fun subscribeMessages(): SubscribeChatRepository = messages } diff --git a/src/main/kotlin/chatserver/ChatServer.kt b/src/main/kotlin/chatserver/ChatServer.kt index 19e9b15..f649021 100644 --- a/src/main/kotlin/chatserver/ChatServer.kt +++ b/src/main/kotlin/chatserver/ChatServer.kt @@ -1,15 +1,12 @@ package chatserver -import chatserver.MessagesRepository.Read -import chatserver.MessagesRepository.Write +import chatserver.messages.chatServerMessagesModule import chatserver.profiles.chatServerProfilesModule import org.koin.core.module.dsl.factoryOf -import org.koin.dsl.binds import org.koin.dsl.module val chatServerModule = module { - includes(chatServerProfilesModule) - factory { MockMessages } binds arrayOf(Read::class, Write::class) + includes(chatServerProfilesModule, chatServerMessagesModule) factoryOf(::ChatRepositories) } diff --git a/src/main/kotlin/chatserver/MessagesRepository.kt b/src/main/kotlin/chatserver/MessagesRepository.kt deleted file mode 100644 index 85298a6..0000000 --- a/src/main/kotlin/chatserver/MessagesRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package chatserver - -import kotlinx.coroutines.flow.Flow - -interface MessagesRepository { - interface Read { - fun observe(): Flow - } - - interface Write { - suspend fun send(message: String) - } -} diff --git a/src/main/kotlin/chatserver/MockMessages.kt b/src/main/kotlin/chatserver/MockMessages.kt deleted file mode 100644 index 88dede6..0000000 --- a/src/main/kotlin/chatserver/MockMessages.kt +++ /dev/null @@ -1,20 +0,0 @@ -package chatserver - -import chatserver.MessagesRepository.Read -import chatserver.MessagesRepository.Write -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow - -object MockMessages : Read, Write { - private val _events = MutableSharedFlow(extraBufferCapacity = 64) - private val events = _events.asSharedFlow() - - override fun observe(): Flow { - return events - } - - override suspend fun send(message: String) { - _events.emit(message) - } -} diff --git a/src/main/kotlin/chatserver/messages/ChatServerMessages.kt b/src/main/kotlin/chatserver/messages/ChatServerMessages.kt new file mode 100644 index 0000000..2f1386c --- /dev/null +++ b/src/main/kotlin/chatserver/messages/ChatServerMessages.kt @@ -0,0 +1,8 @@ +package chatserver.messages + +import org.koin.dsl.module + +val chatServerMessagesModule = + module { + single { LocalChatMessages(get()) } + } diff --git a/src/main/kotlin/chatserver/messages/LocalChatMessages.kt b/src/main/kotlin/chatserver/messages/LocalChatMessages.kt new file mode 100644 index 0000000..d8bf52c --- /dev/null +++ b/src/main/kotlin/chatserver/messages/LocalChatMessages.kt @@ -0,0 +1,92 @@ +package chatserver.messages + +import arch.RokyDispatchers +import chatserver.ChatMessageResult +import chatserver.ReadChatRepository +import chatserver.SubscribeChatRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +class LocalChatMessages( + private val dispatchers: RokyDispatchers, + private val scope: CoroutineScope = CoroutineScope(dispatchers.default + Job()), + private val source: () -> Flow = { emitEveryThreeSeconds(sampleUsers, sampleMessages) }, +) : ReadChatRepository, SubscribeChatRepository { + private var samples: Job? = null + private val _events = MutableStateFlow("") + private val events = _events.asStateFlow() + + override fun latest(): ChatMessageResult = events.toResult() + + override fun observe(): Flow = events.map { ChatMessageResult.ok(it) } + + override fun subscribe() { + samples = + scope.launch { + source().cancellable().collect { + _events.value = it + } + } + } + + override fun unsubscribe() { + samples?.cancel() + } + + companion object { + private fun StateFlow.toResult(): ChatMessageResult = ChatMessageResult.ok(value) + + private val sampleMessages = + listOf( + "This is a coup!", + "What time's Roky Coding tonight?", + "Look at the calendar...", + "Charizard", + "Remind me to get my washing at 4 PM", + "ASMR....", + "BRAIN...praise me", + "Biggleswade is naff", + "Biggleswade is amazing /s", + "I love Biggleswade!!!", + "AHHHHHHHHHHHHHHHHHH", + "Wordle: 4/6", + "Wordle: 1/6", + "Wordle: 2/6", + "I AM SO HUNGRY RN", + "I am feeling quiet today", + ":thumbs_up:", + "I just have a bit of a cold rn", + "I just think it's something going around", + "Don't just type out what I'm saying Mike", + ":breathing_noises:", + ) + + val sampleUsers = + listOf( + "Martine", + "Ed", + "Kai", + "Terry", + "Robert", + "Tom", + "Brian", + "Dunia", + "Stefano", + "Mike", + ) + + private fun emitEveryThreeSeconds( + users: List, + messages: List, + ) = flow { + while (true) { + delay(3.seconds) + emit("${users.random()}: ${messages.random()}") + } + } + } +} diff --git a/src/main/kotlin/chatserver/profiles/LocalProfilesRepository.kt b/src/main/kotlin/chatserver/profiles/LocalProfilesRepository.kt index a977863..759ad0b 100644 --- a/src/main/kotlin/chatserver/profiles/LocalProfilesRepository.kt +++ b/src/main/kotlin/chatserver/profiles/LocalProfilesRepository.kt @@ -1,12 +1,12 @@ package chatserver.profiles import arch.RokyDispatchers -import chatroom.viewmessages.ViewMessagesUseCase import chatserver.ProfileResult import chatserver.ProfileResult.Companion.ok import chatserver.ReadChatRepository import chatserver.SubscribeChatRepository import chatserver.WriteChatRepository +import chatserver.messages.LocalChatMessages import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -32,7 +32,7 @@ class LocalProfilesRepository( override fun subscribe() { scope.launch(dispatchers.default) { while (true) { - val users = ViewMessagesUseCase.sampleUsers.shuffled() + val users = LocalChatMessages.sampleUsers.shuffled() state.value = users.associateWith { it }.let(ProfileResult::ok) delay(5.seconds) } diff --git a/src/test/kotlin/chatroom/sendmessages/SendMessagePresenterTest.kt b/src/test/kotlin/chatroom/sendmessages/SendMessagePresenterTest.kt index e0e0b3d..90a994c 100644 --- a/src/test/kotlin/chatroom/sendmessages/SendMessagePresenterTest.kt +++ b/src/test/kotlin/chatroom/sendmessages/SendMessagePresenterTest.kt @@ -3,7 +3,7 @@ package chatroom.sendmessages import arch.RokyDispatchers import chatroom.sendmessages.SendMessageEvent.SendMessage import chatroom.sendmessages.SendMessageViewState.Clear -import chatserver.MessagesRepository +import io.kotest.matchers.collections.shouldContainExactly import io.mockk.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -16,14 +16,16 @@ import org.junit.jupiter.api.Test @OptIn(ExperimentalCoroutinesApi::class) class SendMessagePresenterTest { private lateinit var scope: CoroutineScope - private lateinit var send: MessagesRepository.Write + private lateinit var messages: MutableList private lateinit var view: SendMessagesView private lateinit var presenter: SendMessagePresenter @BeforeEach fun setUp() { - send = mockk() - coEvery { send.send(any()) } just runs + messages = mutableListOf() + val send: (String) -> Unit = { + messages.add(it) + } view = mockk(relaxed = true) val dispatchers: RokyDispatchers = mockk().apply { @@ -46,7 +48,7 @@ class SendMessagePresenterTest { presenter.attach(view) presenter.onEvent(SendMessage("Hello")) advanceUntilIdle() - coVerify { send.send("Hello") } + messages.shouldContainExactly("Hello") } @Test diff --git a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt index 15bf5c3..ad6d2fb 100644 --- a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt +++ b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt @@ -3,7 +3,9 @@ package chatroom.viewmessages import arch.RokyDispatchers import chatroom.viewmessages.ViewMessagesViewState.Messages import chatroom.viewmessages.ViewMessagesViewState.NoMessages -import coAnswersDelayed +import chatserver.ChatMessageResult +import chatserver.ReadChatRepository +import chatserver.SubscribeChatRepository import io.mockk.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -14,19 +16,22 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class ViewMessagesPresenterTest { - private lateinit var messages: ViewMessagesUseCase + private lateinit var channel: SubscribeChatRepository + private lateinit var read: ReadChatRepository private lateinit var scope: CoroutineScope private lateinit var view: ViewMessagesView private lateinit var presenter: ViewMessagesPresenter @BeforeEach fun setUp() { - messages = mockk(relaxed = true) - coEvery { messages() } coAnswersDelayed { flowOf() } + channel = mockk(relaxed = true) + read = mockk() + every { read.observe() } returns flowOf() view = mockk(relaxed = true) view = mockk(relaxed = true) val dispatchers: RokyDispatchers = @@ -35,7 +40,7 @@ class ViewMessagesPresenterTest { every { io } returns dispatcher } scope = CoroutineScope(dispatcher) - presenter = ViewMessagesPresenter(scope, messages, dispatchers) + presenter = ViewMessagesPresenter(scope, read, channel, dispatchers) } @Test @@ -50,30 +55,63 @@ class ViewMessagesPresenterTest { @Test fun `given message exist, when attached, then show message`() = runTest(dispatcher) { - coEvery { messages() } coAnswersDelayed { flowOf("Biggleswade is beautiful") } + every { read.observe() } returns flowOf(ChatMessageResult.ok("Biggleswade is bad")) presenter.attach(view) advanceUntilIdle() verifyOrder { view.show(NoMessages) - view.show(assertMessage("Biggleswade is beautiful")) + view.show(assertMessage("Biggleswade is bad")) } } + @Test + fun `given message exist, and message is not ok, when attached, then show nothing`() = + runTest(dispatcher) { + every { read.observe() } returns flowOf(ChatMessageResult.fail(RuntimeException("Biggleswade is bad"))) + presenter.attach(view) + advanceUntilIdle() + verifyOrder { + view.show(NoMessages) + } + verify(exactly = 0) { + view.show( + withArg { + assertFalse { it !is Messages } + }, + ) + } + } + + @Test + fun `when attached, then subscribe to chat messages`() = + runTest(dispatcher) { + presenter.attach(view) + verify { channel.subscribe() } + } + + @Test + fun `when detached, then unsubscribe to chat messages`() = + runTest(dispatcher) { + presenter.attach(view) + presenter.detach() + verify { channel.unsubscribe() } + } + @Test fun `given two messages exist, when attached, then show two messages`() = runTest(dispatcher) { - coEvery { messages() } coAnswersDelayed { + every { read.observe() } returns flowOf( - "Biggleswade is beautiful", - "Robert is good at Kotlin", + ChatMessageResult.ok("Biggleswade is bad"), + ChatMessageResult.ok("Robert is good at bad, kai is better"), ) - } + presenter.attach(view) advanceUntilIdle() verifyOrder { view.show(NoMessages) - view.show(assertMessage("Biggleswade is beautiful")) - view.show(assertMessage("Robert is good at Kotlin")) + view.show(assertMessage("Biggleswade is bad")) + view.show(assertMessage("Robert is good at bad, kai is better")) } } diff --git a/src/test/kotlin/chatroom/viewmessages/ViewMessagesUseCaseTest.kt b/src/test/kotlin/chatroom/viewmessages/ViewMessagesUseCaseTest.kt deleted file mode 100644 index b1068d1..0000000 --- a/src/test/kotlin/chatroom/viewmessages/ViewMessagesUseCaseTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package chatroom.viewmessages - -import chatserver.MessagesRepository -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class) -class ViewMessagesUseCaseTest { - private lateinit var messages: MessagesRepository.Read - private lateinit var viewMessages: ViewMessagesUseCase - - @BeforeEach - fun setUp() { - messages = mockk() - every { - messages.observe() - } returns flowOf() - } - - @Test - fun `given messages exist, when observing messages, then receive messages`() = - runTest { - viewMessages = ViewMessagesUseCase(listOf("Rob is a goodie twoshoes!!"), listOf("Brian"), messages) - assertEquals( - expected = "Brian: Rob is a goodie twoshoes!!", - actual = viewMessages().first(), - ) - } - - @Test - fun `given messages exist, when observing multiple messages, then receive multiple messages`() = - runTest { - viewMessages = ViewMessagesUseCase(listOf("Rob is a goodie twoshoes!!"), listOf("Brian"), messages) - val emissions = mutableListOf() - backgroundScope.launch { viewMessages().collect(emissions::add) } - advanceTimeBy(10.seconds) - assertEquals( - expected = 3, - actual = emissions.size, - ) - } - - @Test - fun `given messages exist, when messages interleave, then print all messages`() = - runTest { - every { - messages.observe() - } returns flowOf("Hi there you alright?!") - viewMessages = ViewMessagesUseCase(listOf("Rob is a goodie twoshoes!!"), listOf("Brian"), messages) - val emissions = mutableListOf() - backgroundScope.launch { - viewMessages().collect(emissions::add) - } - advanceTimeBy(10.seconds) - assertEquals(4, emissions.size) - assertContains(emissions, "Me: Hi there you alright?!") - } -} diff --git a/src/test/kotlin/chatserver/messages/LocalChatMessagesTest.kt b/src/test/kotlin/chatserver/messages/LocalChatMessagesTest.kt new file mode 100644 index 0000000..62a9df3 --- /dev/null +++ b/src/test/kotlin/chatserver/messages/LocalChatMessagesTest.kt @@ -0,0 +1,102 @@ +package chatserver.messages + +import arch.RokyDispatchers +import io.kotest.matchers.collections.shouldContainInOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class LocalChatMessagesTest { + private lateinit var scope: CoroutineScope + private lateinit var messages: LocalChatMessages + + @BeforeEach + fun setUp() { + val dispatchers: RokyDispatchers = + mockk().apply { + every { main } returns dispatcher + every { io } returns dispatcher + } + scope = CoroutineScope(dispatcher) + messages = LocalChatMessages(dispatchers, scope) + } + + @Test + fun `given messages exist, when observing messages, then receive messages`() = + runTest(dispatcher) { + messages = LocalChatMessages(dispatchers, scope, kaimitter()) + backgroundScope.launch { messages.subscribe() } + dispatcher.scheduler.advanceTimeBy(10.seconds) + + assertEquals(MSG_2, messages.latest().item) + + messages.unsubscribe() + } + + @Test + fun `when observing multiple messages, then receive same number of messages`() = + runTest(dispatcher) { + messages = LocalChatMessages(dispatchers, scope, kaimitter()) + + val emissions = mutableListOf() + backgroundScope.launch { + messages.observe().map { it.item }.collect(emissions::add) + } + messages.subscribe() + + advanceTimeBy(10.seconds) + + emissions.shouldHaveSize(3) + messages.unsubscribe() + } + + @Test + fun `when observing multiple messages, then receive multiple messages`() = + runTest(dispatcher) { + messages = LocalChatMessages(dispatchers, scope, kaimitter()) + + val emissions = mutableListOf() + backgroundScope.launch { + messages.observe().map { it.item }.collect(emissions::add) + } + messages.subscribe() + + advanceTimeBy(10.seconds) + + emissions.shouldContainInOrder("", MSG_1, MSG_2) + messages.unsubscribe() + } + + companion object { + private val dispatcher = StandardTestDispatcher() + private val dispatchers: RokyDispatchers + get() = + mockk().apply { + every { main } returns dispatcher + every { io } returns dispatcher + } + + private const val USR = "Kai" + private const val CONTENT_1 = "Rob is bad!!" + private const val CONTENT_2 = "Kai is worse!" + private const val MSG_1 = "$USR: $CONTENT_1" + private const val MSG_2 = "$USR: $CONTENT_2" + + private fun kaimitter() = { flowOf(MSG_1, MSG_2).onEach { delay(1.seconds) } } + } +}