package ch.threema.app.groupmanagement

import ch.threema.app.DangerousTest
import ch.threema.app.groupflows.GroupChanges
import ch.threema.app.groupflows.GroupChanges.ProfilePictureChange.NoChange
import ch.threema.app.groupflows.GroupFlowResult
import ch.threema.app.multidevice.MultiDeviceManager
import ch.threema.app.tasks.GroupUpdateTask
import ch.threema.app.tasks.ReflectLocalGroupUpdate
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.domain.taskmanager.TaskManager
import ch.threema.test.TestData.PUBLIC_KEY
import java.time.Instant
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
import kotlinx.coroutines.test.runTest
import org.koin.core.module.Module
import org.koin.dsl.module

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

    /**
     * A contact that is stored in the database. It is the creator of [initialGroupModelData].
     */
    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,
    )

    /**
     * A contact that is stored in the database. An initial group member of
     * [myInitialGroupModelData].
     */
    private val initialGroupMemberData = ContactModelData(
        identity = "TESTTEST",
        publicKey = ByteArray(32) { 2.toByte() },
        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 = setOf(initialGroupMemberData.identity),
        userState = UserState.MEMBER,
        notificationTriggerPolicyOverride = null,
    )

    private val initialGroupModelData = GroupModelData(
        groupIdentity = GroupIdentity(initialContactData.identity, 42),
        name = "ExistingGroup",
        createdAt = Instant.now(),
        synchronizedAt = null,
        lastUpdate = Instant.now(),
        conversationVisibility = ConversationVisibility.NORMAL,
        groupDescription = null,
        groupDescriptionChangedAt = null,
        // User is the only member besides from the creator
        otherMembers = emptySet(),
        userState = UserState.MEMBER,
        notificationTriggerPolicyOverride = null,
    )

    private lateinit var taskManager: ControlledTaskManager
    private lateinit var multiDeviceManager: MultiDeviceManager

    override fun getInstrumentationTestModule(): Module = module {
        factory<TaskManager> { taskManager }
        factory<MultiDeviceManager> { multiDeviceManager }
    }

    @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.apply {
            createFromSync(initialContactData)
            createFromSync(initialGroupMemberData)
        }
        groupModelRepository.apply {
            createFromSync(myInitialGroupModelData)
            createFromSync(initialGroupModelData)
        }
    }

    @Test
    fun testGroupNameModificationMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = myInitialGroupModelData.otherMembers,
            groupModelData = myInitialGroupModelData,
        )

        assertSuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testGroupNameModificationNonMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = myInitialGroupModelData.otherMembers,
            groupModelData = myInitialGroupModelData,
        )

        assertSuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testGroupNameAndAddedMembersModificationMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)

        // Assert that the new member is not yet a member of the group
        assertEquals(false, groupModel.data?.otherMembers?.contains(initialContactData.identity))

        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = myInitialGroupModelData.otherMembers + initialContactData.identity,
            groupModelData = myInitialGroupModelData,
        )

        assertSuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )

        assertEquals(true, groupModel.data?.otherMembers?.contains(initialContactData.identity))
    }

    @Test
    fun testGroupNameAndAddedMembersModificationNonMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = myInitialGroupModelData.otherMembers + initialContactData.identity,
            groupModelData = myInitialGroupModelData,
        )

        assertSuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )

        assertEquals(true, groupModel.data?.otherMembers?.contains(initialContactData.identity))
    }

    @Test
    fun testGroupNameAndRemovedMembersModificationMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = emptySet(),
            groupModelData = myInitialGroupModelData,
        )

        assertSuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )

        assertEquals(true, groupModel.data?.otherMembers?.isEmpty())
    }

    @Test
    fun testGroupNameAndRemovedMembersModificationNonMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = emptySet(),
            groupModelData = myInitialGroupModelData,
        )

        assertSuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )

        assertEquals(true, groupModel.data?.otherMembers?.isEmpty())
    }

    @Test
    fun testModificationOfDeletedGroupMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = emptySet(),
            groupModelData = myInitialGroupModelData,
        )

        // Delete the group before updating it
        groupModelRepository.persistRemovedGroup(groupModel.groupIdentity)
        assertNull(groupModel.data)

        assertUnsuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )

        assertNull(groupModel.data)
    }

    @Test
    fun testModificationOfDeletedGroupNonMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = emptySet(),
            groupModelData = myInitialGroupModelData,
        )

        // Delete the group before updating it
        groupModelRepository.persistRemovedGroup(groupModel.groupIdentity)
        assertNull(groupModel.data)

        assertUnsuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )

        assertNull(groupModel.data)
    }

    @Test
    fun testGroupNameAndAddedMembersModificationOfForeignGroupMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = setOf(initialGroupMemberData.identity),
            groupModelData = initialGroupModelData,
        )

        assertUnsuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )

        assertEquals(false, groupModel.data?.otherMembers?.contains(initialGroupMemberData.identity))
    }

    @Test
    fun testGroupNameAndAddedMembersModificationOfForeignGroupNonMd() = runTest {
        val groupModel = groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        val groupChanges = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = setOf(initialGroupMemberData.identity),
            groupModelData = initialGroupModelData,
        )

        assertUnsuccessfulGroupUpdate(
            groupModel,
            groupChanges,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )

        assertEquals(false, groupModel.data?.otherMembers?.contains(initialGroupMemberData.identity))
    }

    @Test
    fun shouldNotUpdateGroupWhenMdActiveButConnectionIsLost() = runTest {
        // arrange
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        taskManager = ControlledTaskManager(emptyList())
        multiDeviceManager = testMultiDeviceManagerEnabled

        val groupFlowDispatcher = getGroupFlowDispatcher(
            setupConfig = SetupConfig.MULTI_DEVICE_ENABLED,
            taskManager = taskManager,
            connection = ConnectionDisconnected,
        )
        val groupChangesNewName = GroupChanges(
            name = "NewGroupName",
            profilePictureChange = NoChange,
            updatedMembers = myInitialGroupModelData.otherMembers,
            groupModelData = myInitialGroupModelData,
        )

        // act
        val groupFlowResult = groupFlowDispatcher
            .runUpdateGroupFlow(groupChangesNewName, groupModel!!)
            .await()

        // assert
        assertIs<GroupFlowResult.Failure.Network>(groupFlowResult)
    }

    private suspend fun assertSuccessfulGroupUpdate(
        groupModel: GroupModel,
        groupChanges: GroupChanges,
        reflectionExpectation: ReflectionExpectation,
    ) {
        val groupFlowResult = runGroupUpdate(
            groupModel = groupModel,
            groupChanges = groupChanges,
            reflectionExpectation = reflectionExpectation,
            successExpected = true,
        )
        assertIs<GroupFlowResult.Success>(groupFlowResult)

        val groupModelData = groupModel.data
        assertNotNull(groupModelData)
        groupModelData.assertChangesApplied(groupChanges)
    }

    private suspend fun assertUnsuccessfulGroupUpdate(
        groupModel: GroupModel,
        groupChanges: GroupChanges,
        reflectionExpectation: ReflectionExpectation,
    ) {
        val groupFlowResult = runGroupUpdate(
            groupModel = groupModel,
            groupChanges = groupChanges,
            reflectionExpectation = reflectionExpectation,
            successExpected = false,
        )
        assertIs<GroupFlowResult.Failure>(groupFlowResult)
    }

    private suspend fun runGroupUpdate(
        groupModel: GroupModel,
        groupChanges: GroupChanges,
        reflectionExpectation: ReflectionExpectation,
        successExpected: Boolean,
    ): GroupFlowResult {
        val groupModelData = groupModel.data

        // Prepare task manager and group flow dispatcher
        taskManager = ControlledTaskManager(
            getExpectedTaskAssertions(groupModelData, reflectionExpectation, successExpected),
        )
        multiDeviceManager = if (reflectionExpectation.setupConfig == SetupConfig.MULTI_DEVICE_ENABLED) {
            testMultiDeviceManagerEnabled
        } else {
            testMultiDeviceManagerDisabled
        }
        val groupFlowDispatcher = getGroupFlowDispatcher(
            reflectionExpectation.setupConfig,
            taskManager,
        )

        // Run update group flow
        val groupFlowResult = groupFlowDispatcher
            .runUpdateGroupFlow(groupChanges, groupModel)
            .await()

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

        return groupFlowResult
    }

    private fun getExpectedTaskAssertions(
        groupModelData: GroupModelData?,
        reflectionExpectation: ReflectionExpectation,
        successExpected: Boolean,
    ): MutableList<(Task<*, TaskCodec>) -> Unit> {
        if (groupModelData == null) {
            return mutableListOf()
        }

        val scheduledTaskAssertions: MutableList<(Task<*, TaskCodec>) -> Unit> = mutableListOf()
        // If multi device is enabled, then we expect a reflection
        if (reflectionExpectation.setupConfig == SetupConfig.MULTI_DEVICE_ENABLED) {
            scheduledTaskAssertions.add { task ->
                assertIs<ReflectLocalGroupUpdate>(task)
            }
        }

        // If the group is not a notes group, we expect that a task is scheduled that sends out csp
        // messages.
        val isNotesGroup =
            groupModelData.otherMembers.isEmpty() && groupModelData.groupIdentity.creatorIdentity == myContact.identity
        val reflectionFails = reflectionExpectation == ReflectionExpectation.REFLECTION_FAIL
        if (successExpected && !isNotesGroup && !reflectionFails) {
            scheduledTaskAssertions.add { task ->
                assertIs<GroupUpdateTask>(task)
            }
        }

        return scheduledTaskAssertions
    }

    /**
     * Assert that the [groupChanges] have been applied to the group model data.
     */
    private fun GroupModelData.assertChangesApplied(groupChanges: GroupChanges) {
        groupChanges.name?.let { newName ->
            assertEquals(newName, this.name)
        }
        assertContainsAll(this.otherMembers, groupChanges.addMembers)
        assertContainsNone(this.otherMembers, groupChanges.removeMembers)
    }

    private fun <T> assertContainsAll(container: Iterable<T>, elements: Iterable<T>) {
        elements.forEach {
            assertContains(container, it)
        }
    }

    private fun <T> assertContainsNone(container: Iterable<T>, elements: Iterable<T>) {
        val intersection = container.intersect(elements.toSet())
        assertTrue(message = "Elements contained unexpectedly") {
            intersection.isEmpty()
        }
    }
}
