package com.fsck.k9.ui.messagelist

import android.content.Context
import android.text.Spannable
import android.text.style.AbsoluteSizeSpan
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.core.view.isGone
import androidx.core.view.isVisible
import assertk.Assert
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import assertk.assertions.support.expected
import com.fsck.k9.FontSizes
import com.fsck.k9.FontSizes.Companion.FONT_DEFAULT
import com.fsck.k9.FontSizes.Companion.LARGE
import com.fsck.k9.UiDensity
import com.fsck.k9.contacts.ContactPictureLoader
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.ui.R
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
import com.google.android.material.textview.MaterialTextView
import kotlin.time.ExperimentalTime
import net.thunderbird.core.android.account.Identity
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.android.testing.RobolectricTest
import net.thunderbird.core.featureflag.FeatureFlagKey
import net.thunderbird.core.featureflag.FeatureFlagProvider
import net.thunderbird.core.featureflag.FeatureFlagResult
import net.thunderbird.core.testing.TestClock
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
import net.thunderbird.feature.account.AccountIdFactory
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
import net.thunderbird.feature.account.storage.profile.ProfileDto
import org.junit.Test
import org.mockito.kotlin.mock
import org.robolectric.Robolectric

private const val SOME_ACCOUNT_UUID = "6b84207b-25de-4dab-97c3-953bbf03fec6"
private const val FIRST_LINE_DEFAULT_FONT_SIZE = 16f
private const val SECOND_LINE_DEFAULT_FONT_SIZE = 14f
private const val DATE_DEFAULT_FONT_SIZE = 14f

class MessageListAdapterTest : RobolectricTest() {
    val activity = Robolectric.buildActivity(AppCompatActivity::class.java).create().get()
    val context: Context = ContextThemeWrapper(activity, com.google.android.material.R.style.Theme_Material3_Light)

    val contactsPictureLoader: ContactPictureLoader = mock()
    val listItemListener: MessageListItemActionListener = mock()

    @Test
    fun withShowAccountIndicator_shouldShowAccountIndicator() {
        val adapter = createAdapter(showAccountIndicator = true)

        val view = adapter.createAndBindView()

        assertThat(view.accountIndicatorView).isVisible()
    }

    @Test
    fun withoutShowAccountIndicator_shouldHideAccountIndicator() {
        val adapter = createAdapter(showAccountIndicator = false)

        val view = adapter.createAndBindView()

        assertThat(view.accountIndicatorView).isGone()
    }

    @Test
    fun withoutStars_shouldHideStarView() {
        val adapter = createAdapter(stars = false)

        val view = adapter.createAndBindView()

        assertThat(view.starView).isGone()
    }

    @Test
    fun withStars_shouldShowStarView() {
        val adapter = createAdapter(stars = true)

        val view = adapter.createAndBindView()

        assertThat(view.starView).isVisible()
    }

    @Test
    fun withStarsAndStarredMessage_shouldSetStarViewToSelected() {
        val adapter = createAdapter(stars = true)
        val messageListItem = createMessageListItem(isStarred = true)

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.starView).isSelected()
    }

    @Test
    fun withStarsAndUnstarredMessage_shouldNotSetStarViewToSelected() {
        val adapter = createAdapter(stars = true)
        val messageListItem = createMessageListItem(isStarred = false)

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.starView).isNotSelected()
    }

    @Test
    fun withoutShowContactPicture_shouldHideContactPictureView() {
        val adapter = createAdapter(showContactPicture = false)

        val view = adapter.createAndBindView()

        assertThat(view.contactPictureContainerView).isGone()
    }

    @Test
    fun withShowContactPicture_shouldShowContactPictureView() {
        val adapter = createAdapter(showContactPicture = true)

        val view = adapter.createAndBindView()

        assertThat(view.contactPictureContainerView).isVisible()
    }

    @Test
    fun withThreadCountOne_shouldHideThreadCountView() {
        val adapter = createAdapter()
        val messageListItem = createMessageListItem(threadCount = 1)

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.threadCountView).isGone()
    }

    @Test
    fun withThreadCountGreaterOne_shouldShowThreadCountViewWithExpectedValue() {
        val adapter = createAdapter()
        val messageListItem = createMessageListItem(threadCount = 13)

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.threadCountView).isVisible()
        assertThat(view.threadCountView.textString).isEqualTo("13")
    }

    @Test
    fun withoutSenderAboveSubject_shouldShowSubjectInFirstLine() {
        val adapter = createAdapter(senderAboveSubject = false)
        val messageListItem = createMessageListItem(subject = "Subject")

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.firstLineView.textString).isEqualTo("Subject")
    }

    @Test
    fun withSenderAboveSubject_shouldShowDisplayNameInFirstLine() {
        val adapter = createAdapter(senderAboveSubject = true)
        val messageListItem = createMessageListItem(displayName = "Display Name")

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.firstLineView.textString).isEqualTo("Display Name")
    }

    @Test
    fun withoutSenderAboveSubjectAndZeroPreviewLines_shouldShowDisplayNameInSecondLine() {
        val adapter = createAdapter(senderAboveSubject = false, previewLines = 0)
        val messageListItem = createMessageListItem(displayName = "Display Name")

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.secondLineView.textString).isEqualTo("Display Name")
    }

    @Test
    fun withoutSenderAboveSubjectAndPreviewLines_shouldShowDisplayNameAndPreviewInSecondLine() {
        val adapter = createAdapter(senderAboveSubject = false, previewLines = 1)
        val messageListItem = createMessageListItem(displayName = "Display Name", previewText = "Preview")

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.secondLineView.textString).isEqualTo(secondLine("Display Name", "Preview"))
    }

    @Test
    fun withSenderAboveSubjectAndZeroPreviewLines_shouldShowSubjectInSecondLine() {
        val adapter = createAdapter(senderAboveSubject = true, previewLines = 0)
        val messageListItem = createMessageListItem(subject = "Subject")

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.secondLineView.textString).isEqualTo("Subject")
    }

    @Test
    fun withSenderAboveSubjectAndPreviewLines_shouldShowSubjectAndPreviewInSecondLine() {
        val adapter = createAdapter(senderAboveSubject = true, previewLines = 1)
        val messageListItem = createMessageListItem(subject = "Subject", previewText = "Preview")

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.secondLineView.textString).isEqualTo(secondLine("Subject", "Preview"))
    }

    @Test
    fun withMissingSubject_shouldDisplayNoSubjectIndicator() {
        val adapter = createAdapter(senderAboveSubject = false)
        val messageListItem = createMessageListItem(subject = null)

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.firstLineView.textString).isMissingSubjectText()
    }

    @Test
    fun withEmptySubject_shouldDisplayNoSubjectIndicator() {
        val adapter = createAdapter(senderAboveSubject = false)
        val messageListItem = createMessageListItem(subject = "")

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.firstLineView.textString).isMissingSubjectText()
    }

    @Test
    fun withoutAttachments_shouldHideAttachmentCountView() {
        val adapter = createAdapter()
        val messageListItem = createMessageListItem(hasAttachments = false)

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.attachmentCountView).isGone()
    }

    @Test
    fun withAttachments_shouldShowAttachmentCountView() {
        val adapter = createAdapter()
        val messageListItem = createMessageListItem(hasAttachments = true)

        val view = adapter.createAndBindView(messageListItem)

        assertThat(view.attachmentCountView).isVisible()
    }

    @Test
    fun withoutSenderAboveSubjectAndDefaultFontSize_shouldNotSetTextSizeOfFirstLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(subject = FONT_DEFAULT),
            senderAboveSubject = false,
        )

        val view = adapter.createAndBindView()

        assertThat(view.firstLineView.textSize).isEqualTo(FIRST_LINE_DEFAULT_FONT_SIZE)
    }

    @Test
    fun withoutSenderAboveSubjectAndNonDefaultFontSize_shouldSetTextSizeOfFirstLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(subject = LARGE),
            senderAboveSubject = false,
        )

        val view = adapter.createAndBindView()

        assertThat(view.firstLineView.textSize).isEqualTo(22f)
    }

    @Test
    fun withSenderAboveSubjectAndDefaultFontSize_shouldNotSetTextSizeOfFirstLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(sender = FONT_DEFAULT),
            senderAboveSubject = true,
        )

        val view = adapter.createAndBindView()

        assertThat(view.firstLineView.textSize).isEqualTo(FIRST_LINE_DEFAULT_FONT_SIZE)
    }

    @Test
    fun withSenderAboveSubjectAndNonDefaultFontSize_shouldSetTextSizeOfFirstLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(sender = LARGE),
            senderAboveSubject = true,
        )

        val view = adapter.createAndBindView()

        assertThat(view.firstLineView.textSize).isEqualTo(22f)
    }

    @Test
    fun withoutSenderAboveSubjectAndDefaultFontSize_shouldNotSetTextSizeSpanInSecondLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(sender = FONT_DEFAULT),
            senderAboveSubject = false,
        )

        val view = adapter.createAndBindView()

        assertThat(view.secondLineView.text).firstAbsoluteSizeSpanValue().isNull()
    }

    @Test
    fun withoutSenderAboveSubjectAndNonDefaultFontSize_shouldSetTextSizeSpanInSecondLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(sender = LARGE),
            senderAboveSubject = false,
        )

        val view = adapter.createAndBindView()

        assertThat(view.secondLineView.text).firstAbsoluteSizeSpanValue().isEqualTo(22)
    }

    @Test
    fun withSenderAboveSubjectAndDefaultFontSize_shouldNotSetTextSizeSpanInSecondLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(subject = FONT_DEFAULT),
            senderAboveSubject = true,
        )

        val view = adapter.createAndBindView()

        assertThat(view.secondLineView.text).firstAbsoluteSizeSpanValue().isNull()
    }

    @Test
    fun withSenderAboveSubjectAndNonDefaultFontSize_shouldSetTextSizeSpanInSecondLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(subject = LARGE),
            senderAboveSubject = true,
        )

        val view = adapter.createAndBindView()

        assertThat(view.secondLineView.text).firstAbsoluteSizeSpanValue().isEqualTo(22)
    }

    @Test
    fun dateWithDefaultFontSize_shouldNotSetTextSizeOfDateView() {
        val adapter = createAdapter(fontSizes = createFontSizes(date = FONT_DEFAULT))

        val view = adapter.createAndBindView()

        assertThat(view.dateView.textSize).isEqualTo(DATE_DEFAULT_FONT_SIZE)
    }

    @Test
    fun dateWithNonDefaultFontSize_shouldSetTextSizeOfDateView() {
        val adapter = createAdapter(fontSizes = createFontSizes(date = LARGE))

        val view = adapter.createAndBindView()

        assertThat(view.dateView.textSize).isEqualTo(22f)
    }

    @Test
    fun previewWithDefaultFontSize_shouldNotSetTextSizeOfSecondLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(preview = FONT_DEFAULT),
            previewLines = 1,
        )

        val view = adapter.createAndBindView()

        assertThat(view.secondLineView.textSize).isEqualTo(SECOND_LINE_DEFAULT_FONT_SIZE)
    }

    @Test
    fun previewWithNonDefaultFontSize_shouldSetTextSizeOfSecondLineView() {
        val adapter = createAdapter(
            fontSizes = createFontSizes(preview = LARGE),
            previewLines = 1,
        )

        val view = adapter.createAndBindView()

        assertThat(view.secondLineView.textSize).isEqualTo(22f)
    }

    fun createFontSizes(
        subject: Int = FONT_DEFAULT,
        sender: Int = FONT_DEFAULT,
        preview: Int = FONT_DEFAULT,
        date: Int = FONT_DEFAULT,
    ): FontSizes {
        return FontSizes().apply {
            messageListSubject = subject
            messageListSender = sender
            messageListPreview = preview
            messageListDate = date
        }
    }

    fun createAdapter(
        fontSizes: FontSizes = createFontSizes(),
        previewLines: Int = 0,
        stars: Boolean = true,
        senderAboveSubject: Boolean = false,
        showContactPicture: Boolean = true,
        showingThreadedList: Boolean = true,
        backGroundAsReadIndicator: Boolean = false,
        showAccountIndicator: Boolean = false,
        density: UiDensity = UiDensity.Default,
    ): MessageListAdapter {
        val appearance = MessageListAppearance(
            fontSizes,
            previewLines,
            stars,
            senderAboveSubject,
            showContactPicture,
            showingThreadedList,
            backGroundAsReadIndicator,
            showAccountIndicator,
            density,
        )

        @OptIn(ExperimentalTime::class)
        return MessageListAdapter(
            theme = context.theme,
            res = context.resources,
            layoutInflater = LayoutInflater.from(context),
            contactsPictureLoader = contactsPictureLoader,
            listItemListener = listItemListener,
            appearance = appearance,
            relativeDateTimeFormatter = RelativeDateTimeFormatter(context, TestClock()),
            themeProvider = FakeThemeProvider(),
            featureFlagProvider = FakeFeatureFlagProvider(),
            avatarMonogramCreator = mock(),
            contactRepository = mock(),
        )
    }

    fun createMessageListItem(
        account: LegacyAccount = createLegacyAccount(),
        subject: String? = "irrelevant",
        threadCount: Int = 0,
        messageDate: Long = 0L,
        internalDate: Long = 0L,
        displayName: CharSequence = "irrelevant",
        displayAddress: Address? = Address.parse("irrelevant@domain.example").first(),
        previewText: String = "irrelevant",
        isMessageEncrypted: Boolean = false,
        isRead: Boolean = false,
        isStarred: Boolean = false,
        isAnswered: Boolean = false,
        isForwarded: Boolean = false,
        hasAttachments: Boolean = false,
        uniqueId: Long = 0L,
        folderId: Long = 0L,
        messageUid: String = "irrelevant",
        databaseId: Long = 0L,
        threadRoot: Long = 0L,
    ): MessageListItem {
        return MessageListItem(
            account,
            subject,
            threadCount,
            messageDate,
            internalDate,
            displayName,
            displayAddress,
            previewText,
            isMessageEncrypted,
            isRead,
            isStarred,
            isAnswered,
            isForwarded,
            hasAttachments,
            uniqueId,
            folderId,
            messageUid,
            databaseId,
            threadRoot,
        )
    }

    private fun createLegacyAccount(): LegacyAccount {
        return LegacyAccount(
            id = AccountIdFactory.of(SOME_ACCOUNT_UUID),
            name = "irrelevant",
            email = "irrelevant@example.com",
            profile = createProfile(),
            incomingServerSettings = createServerSettings(),
            outgoingServerSettings = createServerSettings(),
            identities = listOf(
                Identity(
                    description = null,
                    name = "irrelevant",
                    email = "irrelevant@example.com",
                ),
            ),
        )
    }

    private fun createProfile(): ProfileDto {
        return ProfileDto(
            id = AccountIdFactory.of(SOME_ACCOUNT_UUID),
            name = "irrelevant",
            color = 0xFF00FF,
            avatar = AvatarDto(
                avatarType = AvatarTypeDto.MONOGRAM,
                avatarMonogram = "ab",
                avatarImageUri = null,
                avatarIconName = null,
            ),
        )
    }

    private fun createServerSettings(): ServerSettings {
        return ServerSettings(
            type = "imap",
            host = "irrelevant",
            port = 993,
            connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
            authenticationType = AuthType.PLAIN,
            username = "irrelevant",
            password = "irrelevant",
            clientCertificateAlias = null,
        )
    }

    fun MessageListAdapter.createAndBindView(item: MessageListItem = createMessageListItem()): View {
        viewItems = listOf(MessageListViewItem.Message(item))
        val holder = onCreateViewHolder(LinearLayout(context), 0)
        onBindViewHolder(holder, 0)
        return holder.itemView
    }

    fun secondLine(senderOrSubject: String, preview: String) = "$senderOrSubject – $preview"

    val View.accountIndicatorView: View get() = findViewById(R.id.account_color_chip)
    val View.starView: View get() = findViewById(R.id.star)
    val View.contactPictureContainerView: View get() = findViewById(R.id.contact_picture_click_area)
    val View.threadCountView: MaterialTextView get() = findViewById(R.id.thread_count)
    val View.firstLineView: MaterialTextView get() = findViewById(R.id.subject)
    val View.secondLineView: MaterialTextView get() = findViewById(R.id.preview)
    val View.attachmentCountView: View get() = findViewById(R.id.attachment)
    val View.dateView: MaterialTextView get() = findViewById(R.id.date)

    private fun Assert<View>.isVisible() = given { actual ->
        if (!actual.isVisible) {
            expected("View to be visible ($actual)")
        }
    }

    private fun Assert<View>.isGone() = given { actual ->
        if (!actual.isGone) {
            expected("View to be gone ($actual)")
        }
    }

    private fun Assert<View>.isSelected() = given { actual ->
        if (!actual.isSelected) {
            expected("View to be selected ($actual)")
        }
    }

    private fun Assert<View>.isNotSelected() = given { actual ->
        if (actual.isSelected) {
            expected("View to not be selected ($actual)")
        }
    }

    private fun Assert<CharSequence>.isMissingSubjectText() {
        isEqualTo(context.getString(R.string.general_no_subject))
    }

    private fun Assert<CharSequence>.firstAbsoluteSizeSpanValue() = transform { text ->
        val spans = (text as Spannable).getSpans(0, text.length, AbsoluteSizeSpan::class.java)
        spans.firstOrNull()?.size
    }

    private val MaterialTextView.textString: String
        get() = text.toString()

    private class FakeThemeProvider : FeatureThemeProvider {
        @Composable
        override fun WithTheme(content: @Composable (() -> Unit)) {
            content()
        }

        @Composable
        override fun WithTheme(
            darkTheme: Boolean,
            content: @Composable (() -> Unit),
        ) {
            content()
        }
    }

    private class FakeFeatureFlagProvider : FeatureFlagProvider {
        // Disabled as the test is primarily concerned with the XML based UI
        override fun provide(key: FeatureFlagKey): FeatureFlagResult = FeatureFlagResult.Disabled
    }
}
