package ch.threema.app.groupmanagement

import ch.threema.app.DangerousTest
import ch.threema.app.groupflows.GroupFlowResult
import ch.threema.app.tasks.ActiveGroupStateResyncTask
import ch.threema.app.testutils.TestHelpers
import ch.threema.app.testutils.clearDatabaseAndCaches
import ch.threema.data.datatypes.AvailabilityStatus
import ch.threema.data.datatypes.ConversationVisibility
import ch.threema.data.datatypes.GroupIdentity
import ch.threema.data.models.ContactModelData
import ch.threema.data.models.GroupModel
import ch.threema.data.models.GroupModelData
import ch.threema.domain.helpers.ControlledTaskManager
import ch.threema.domain.models.AcquaintanceLevel
import ch.threema.domain.models.ContactSyncState
import ch.threema.domain.models.IdentityState
import ch.threema.domain.models.IdentityType
import ch.threema.domain.models.ReadReceiptPolicy
import ch.threema.domain.models.TypingIndicatorPolicy
import ch.threema.domain.models.UserState
import ch.threema.domain.models.VerificationLevel
import ch.threema.domain.models.WorkVerificationLevel
import ch.threema.domain.taskmanager.Task
import ch.threema.domain.taskmanager.TaskCodec
import ch.threema.test.TestData.PUBLIC_KEY
import java.time.Instant
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.fail
import kotlinx.coroutines.test.runTest

@DangerousTest
class GroupResyncFlowTest : GroupFlowTest() {
    private val myContact = TestHelpers.TEST_CONTACT

    private val initialContactData = ContactModelData(
        identity = "12345678",
        publicKey = PUBLIC_KEY,
        createdAt = Instant.now(),
        lastUpdateAt = null,
        firstName = "",
        lastName = "",
        verificationLevel = VerificationLevel.SERVER_VERIFIED,
        workVerificationLevel = WorkVerificationLevel.NONE,
        nickname = null,
        identityType = IdentityType.REGULAR,
        acquaintanceLevel = AcquaintanceLevel.DIRECT,
        activityState = IdentityState.ACTIVE,
        syncState = ContactSyncState.INITIAL,
        featureMask = 255u,
        readReceiptPolicy = ReadReceiptPolicy.DEFAULT,
        typingIndicatorPolicy = TypingIndicatorPolicy.DEFAULT,
        conversationVisibility = ConversationVisibility.NORMAL,
        profilePictureBlobId = null,
        androidContactLookupInfo = null,
        localAvatarExpires = null,
        isRestored = false,
        jobTitle = null,
        department = null,
        notificationTriggerPolicyOverride = null,
        availabilityStatus = AvailabilityStatus.None,
        workLastFullSyncAt = null,
    )

    private val myInitialGroupModelData = GroupModelData(
        groupIdentity = GroupIdentity(myContact.identity, 42),
        name = "MyExistingGroup",
        createdAt = Instant.now(),
        synchronizedAt = null,
        lastUpdate = Instant.now(),
        conversationVisibility = ConversationVisibility.NORMAL,
        groupDescription = null,
        groupDescriptionChangedAt = null,
        otherMembers = emptySet(),
        userState = UserState.MEMBER,
        notificationTriggerPolicyOverride = null,
    )

    private val initialGroupModelData = myInitialGroupModelData.copy(
        groupIdentity = GroupIdentity(initialContactData.identity, 43),
        name = "ExistingGroup",
    )

    @BeforeTest
    fun setup() {
        clearDatabaseAndCaches(serviceManager)

        assert(myContact.identity == TestHelpers.ensureIdentity(serviceManager))

        // Note that we use from sync to prevent any reflection. This is only acceptable in tests.
        contactModelRepository.createFromSync(initialContactData)
        groupModelRepository.apply {
            createFromSync(myInitialGroupModelData)
            createFromSync(initialGroupModelData)
        }
    }

    @Test
    fun testGroupResyncMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertSuccessfulGroupResync(groupModel, SetupConfig.MULTI_DEVICE_ENABLED)
    }

    @Test
    fun testGroupResyncNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertSuccessfulGroupResync(groupModel, SetupConfig.MULTI_DEVICE_DISABLED)
    }

    @Test
    fun testForeignGroupResyncMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulGroupResync(groupModel, SetupConfig.MULTI_DEVICE_ENABLED)
    }

    @Test
    fun testForeignGroupResyncNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulGroupResync(groupModel, SetupConfig.MULTI_DEVICE_DISABLED)
    }

    private suspend fun assertSuccessfulGroupResync(
        groupModel: GroupModel,
        setupConfig: SetupConfig,
    ) {
        assertIs<GroupFlowResult.Success>(
            runGroupResync(groupModel, setupConfig),
        )
    }

    private suspend fun assertUnsuccessfulGroupResync(
        groupModel: GroupModel,
        setupConfig: SetupConfig,
    ) {
        assertIs<GroupFlowResult.Failure>(
            runGroupResync(groupModel, setupConfig),
        )
    }

    private suspend fun runGroupResync(
        groupModel: GroupModel,
        setupConfig: SetupConfig,
    ): GroupFlowResult {
        val groupModelData = groupModel.data

        // Prepare task manager and group flow dispatcher
        val taskManager = ControlledTaskManager(
            getExpectedTaskAssertions(groupModelData),
        )
        val groupFlowDispatcher = getGroupFlowDispatcher(
            setupConfig,
            taskManager,
        )

        // Run group resync flow
        val groupFowResult = groupFlowDispatcher.runGroupResyncFlow(groupModel).await()

        taskManager.pendingTaskAssertions.size.let { size ->
            if (size > 0) {
                fail("There are $size pending task assertions left")
            }
        }

        return groupFowResult
    }

    private fun getExpectedTaskAssertions(groupModelData: GroupModelData?): MutableList<(Task<*, TaskCodec>) -> Unit> {
        if (groupModelData == null ||
            groupModelData.groupIdentity.creatorIdentity != myContact.identity ||
            groupModelData.userState != UserState.MEMBER
        ) {
            return mutableListOf()
        }

        return mutableListOf({ task -> assertIs<ActiveGroupStateResyncTask>(task) })
    }
}
