package com.fsck.k9.storage.messages

import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import assertk.assertions.isNotIn
import assertk.assertions.isNull
import com.fsck.k9.mail.crlf
import com.fsck.k9.mailstore.StorageManager
import com.fsck.k9.storage.RobolectricTest
import okio.buffer
import okio.sink
import okio.source
import org.junit.After
import org.junit.Test
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock

private const val ACCOUNT_UUID = "00000000-0000-4000-0000-000000000000"

class CopyMessageOperationsTest : RobolectricTest() {
    private val messagePartDirectory = createRandomTempDirectory()
    private val sqliteDatabase = createDatabase()
    private val storageManager = mock<StorageManager> {
        on { getAttachmentDirectory(eq(ACCOUNT_UUID), anyOrNull()) } doReturn messagePartDirectory
    }
    private val lockableDatabase = createLockableDatabaseMock(sqliteDatabase)
    private val attachmentFileManager = AttachmentFileManager(storageManager, ACCOUNT_UUID)
    private val threadMessageOperations = ThreadMessageOperations()
    private val copyMessageOperations = CopyMessageOperations(
        lockableDatabase,
        attachmentFileManager,
        threadMessageOperations,
    )

    @After
    fun tearDown() {
        messagePartDirectory.deleteRecursively()
    }

    @Test
    fun `copy message that is part of a thread`() {
        val sourceMessagePartId1 = sqliteDatabase.createMessagePart(
            seq = 0,
            dataLocation = DataLocation.CHILD_PART_CONTAINS_DATA,
            mimeType = "multipart/mixed",
            boundary = "--boundary",
            header = "Message-ID: <msg0002@domain.example>\nIn-Reply-To: <msg0001@domain.example>\n".crlf(),
        )
        val sourceMessagePartId2 = sqliteDatabase.createMessagePart(
            seq = 1,
            root = sourceMessagePartId1,
            parent = sourceMessagePartId1,
            mimeType = "text/plain",
            dataLocation = DataLocation.IN_DATABASE,
            data = "Text part".toByteArray(),
        )
        val sourceMessagePartId3 = sqliteDatabase.createMessagePart(
            seq = 2,
            root = sourceMessagePartId1,
            parent = sourceMessagePartId1,
            mimeType = "application/octet-stream",
            dataLocation = DataLocation.ON_DISK,
        )
        attachmentFileManager.getAttachmentFile(sourceMessagePartId3).sink().buffer().use { sink ->
            sink.writeUtf8("Part contents")
        }

        val messageId1 = sqliteDatabase.createMessage(
            folderId = 1,
            empty = true,
            messageIdHeader = "<msg0001@domain.example>",
        )
        val messageId2 = sqliteDatabase.createMessage(
            folderId = 1,
            empty = false,
            messageIdHeader = "<msg0002@domain.example>",
            messagePartId = sourceMessagePartId1,
        )
        val messageId3 = sqliteDatabase.createMessage(
            folderId = 1,
            empty = false,
            messageIdHeader = "<msg0003@domain.example>",
        )
        val threadId1 = sqliteDatabase.createThread(messageId1)
        val threadId2 = sqliteDatabase.createThread(messageId2, root = threadId1, parent = threadId1)
        val threadId3 = sqliteDatabase.createThread(messageId3, root = threadId1, parent = threadId2)

        val destinationMessageId = copyMessageOperations.copyMessage(messageId = messageId2, destinationFolderId = 2)

        assertThat(destinationMessageId).isNotIn(setOf(messageId1, messageId2, messageId3))

        val threads = sqliteDatabase.readThreads()
        assertThat(threads).hasSize(3 + 2)

        val destinationMessageThread = threads.first { it.messageId == destinationMessageId }
        assertThat(destinationMessageThread.id).isNotIn(setOf(threadId1, threadId2, threadId3))
        assertThat(destinationMessageThread.parent).isEqualTo(destinationMessageThread.root)

        val destinationRootThread = threads.first { it.id == destinationMessageThread.root }
        assertThat(destinationRootThread.messageId).isNotIn(setOf(messageId1, messageId2, messageId3))
        assertThat(destinationRootThread.root).isEqualTo(destinationRootThread.id)
        assertThat(destinationRootThread.parent).isNull()

        val messages = sqliteDatabase.readMessages()
        val destinationRootThreadMessage = messages.first { it.id == destinationRootThread.messageId }
        assertThat(destinationRootThreadMessage.empty).isEqualTo(1)
        assertThat(destinationRootThreadMessage.folderId).isEqualTo(2)
        assertThat(destinationRootThreadMessage.messageId).isEqualTo("<msg0001@domain.example>")

        val destinationMessage = messages.first { it.id == destinationMessageThread.messageId }
        val sourceMessage = messages.first { it.id == messageId2 }
        assertThat(destinationMessage).isEqualTo(
            sourceMessage.copy(
                id = destinationMessageId,
                uid = destinationMessage.uid,
                folderId = 2,
                messagePartId = destinationMessage.messagePartId,
            ),
        )

        val messageParts = sqliteDatabase.readMessageParts()
        assertThat(messageParts).hasSize(3 + 3)

        val sourceMessagePart1 = messageParts.first { it.id == sourceMessagePartId1 }
        val sourceMessagePart2 = messageParts.first { it.id == sourceMessagePartId2 }
        val sourceMessagePart3 = messageParts.first { it.id == sourceMessagePartId3 }
        val destinationMessagePart1 = messageParts.first { it.id == destinationMessage.messagePartId }
        val destinationMessagePart2 = messageParts.first { it.root == destinationMessage.messagePartId && it.seq == 1 }
        val destinationMessagePart3 = messageParts.first { it.root == destinationMessage.messagePartId && it.seq == 2 }
        assertThat(destinationMessagePart1).isNotIn(setOf(sourceMessagePart1, sourceMessagePart2, sourceMessagePart3))
        assertThat(destinationMessagePart1).isEqualTo(
            sourceMessagePart1.copy(
                id = destinationMessagePart1.id,
                root = destinationMessagePart1.id,
                parent = -1,
            ),
        )
        assertThat(destinationMessagePart2).isNotIn(setOf(sourceMessagePart1, sourceMessagePart2, sourceMessagePart3))
        assertThat(destinationMessagePart2).isEqualTo(
            sourceMessagePart2.copy(
                id = destinationMessagePart2.id,
                root = destinationMessagePart1.id,
                parent = destinationMessagePart1.id,
            ),
        )
        assertThat(destinationMessagePart3).isNotIn(setOf(sourceMessagePart1, sourceMessagePart2, sourceMessagePart3))
        assertThat(destinationMessagePart3).isEqualTo(
            sourceMessagePart3.copy(
                id = destinationMessagePart3.id,
                root = destinationMessagePart1.id,
                parent = destinationMessagePart1.id,
            ),
        )

        val files = messagePartDirectory.list()?.toList() ?: emptyList()
        assertThat(files).hasSize(2)

        attachmentFileManager.getAttachmentFile(destinationMessagePart3.id!!).source().buffer().use { source ->
            assertThat(source.readUtf8()).isEqualTo("Part contents")
        }
    }

    @Test
    fun `copy message into an existing thread`() {
        val sourceMessagePartId = sqliteDatabase.createMessagePart(
            header = "Message-ID: <msg0002@domain.example>\nIn-Reply-To: <msg0001@domain.example>\n".crlf(),
            mimeType = "text/plain",
            dataLocation = DataLocation.IN_DATABASE,
            data = "Text part".toByteArray(),
        )
        attachmentFileManager.getAttachmentFile(sourceMessagePartId).sink().buffer().use { sink ->
            sink.writeUtf8("Part contents")
        }

        val sourceMessageId = sqliteDatabase.createMessage(
            folderId = 1,
            empty = false,
            messageIdHeader = "<msg0002@domain.example>",
            messagePartId = sourceMessagePartId,
        )
        val destinationMessageId = sqliteDatabase.createMessage(
            folderId = 2,
            empty = true,
            messageIdHeader = "<msg0002@domain.example>",
        )
        val otherDestinationMessageId = sqliteDatabase.createMessage(
            folderId = 2,
            empty = false,
            messageIdHeader = "<msg0003@domain.example>",
        )
        val destinationThreadId = sqliteDatabase.createThread(destinationMessageId)
        val otherDestinationThreadId = sqliteDatabase.createThread(
            otherDestinationMessageId,
            root = destinationThreadId,
            parent = destinationThreadId,
        )

        val resultMessageId = copyMessageOperations.copyMessage(messageId = sourceMessageId, destinationFolderId = 2)

        assertThat(resultMessageId).isEqualTo(destinationMessageId)

        val threads = sqliteDatabase.readThreads()
        assertThat(threads).hasSize(2 + 1)

        val destinationThread = threads.first { it.messageId == destinationMessageId }
        assertThat(destinationThread.id).isEqualTo(destinationThreadId)
        assertThat(destinationThread.parent).isEqualTo(destinationThread.root)

        val destinationRootThread = threads.first { it.id == destinationThread.root }
        assertThat(destinationRootThread.root).isEqualTo(destinationRootThread.id)
        assertThat(destinationRootThread.parent).isNull()

        val otherDestinationThread = threads.first { it.id == otherDestinationThreadId }
        assertThat(otherDestinationThread.root).isEqualTo(destinationRootThread.id)
        assertThat(otherDestinationThread.parent).isEqualTo(destinationThread.id)

        val messages = sqliteDatabase.readMessages()
        assertThat(messages).hasSize(3 + 1)

        val destinationRootThreadMessage = messages.first { it.id == destinationRootThread.messageId }
        assertThat(destinationRootThreadMessage.empty).isEqualTo(1)
        assertThat(destinationRootThreadMessage.folderId).isEqualTo(2)
        assertThat(destinationRootThreadMessage.messageId).isEqualTo("<msg0001@domain.example>")

        val destinationMessage = messages.first { it.id == destinationMessageId }
        val sourceMessage = messages.first { it.id == sourceMessageId }
        assertThat(destinationMessage).isEqualTo(
            sourceMessage.copy(
                id = destinationMessageId,
                uid = destinationMessage.uid,
                folderId = 2,
                messagePartId = destinationMessage.messagePartId,
            ),
        )

        val messageParts = sqliteDatabase.readMessageParts()
        assertThat(messageParts).hasSize(1 + 1)

        val sourceMessagePart = messageParts.first { it.id == sourceMessagePartId }
        val destinationMessagePart = messageParts.first { it.id == destinationMessage.messagePartId }
        assertThat(destinationMessagePart).isEqualTo(
            sourceMessagePart.copy(
                id = destinationMessagePart.id,
                root = destinationMessagePart.id,
                parent = -1,
            ),
        )
    }
}
