package ch.threema.app.groupmanagement

import ch.threema.app.DangerousTest
import ch.threema.app.groupflows.GroupDisbandIntent
import ch.threema.app.groupflows.GroupFlowResult
import ch.threema.app.tasks.OutgoingGroupDisbandTask
import ch.threema.app.tasks.ReflectGroupSyncDeleteTask
import ch.threema.app.tasks.ReflectLocalGroupLeaveOrDisband
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.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
import kotlinx.coroutines.test.runTest

@DangerousTest
class DisbandGroupFlowTest : 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 = setOf(initialContactData.identity),
        userState = UserState.MEMBER,
        notificationTriggerPolicyOverride = null,
    )

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

    private val myInitialLeftGroupModelData = myInitialGroupModelData.copy(
        groupIdentity = GroupIdentity(myContact.identity, 44),
        name = "LeftGroup",
        userState = UserState.LEFT,
    )

    private val myInitialKickedGroupModelData = myInitialGroupModelData.copy(
        groupIdentity = GroupIdentity(myContact.identity, 45),
        name = "KickedGroup",
        userState = UserState.KICKED,
    )

    private val myInitialNotesGroupModelData = myInitialGroupModelData.copy(
        groupIdentity = GroupIdentity(myContact.identity, 46),
        name = "NotesGroup",
        userState = UserState.MEMBER,
        otherMembers = emptySet(),
    )

    @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)
            createFromSync(myInitialLeftGroupModelData)
            createFromSync(myInitialKickedGroupModelData)
            createFromSync(myInitialNotesGroupModelData)
        }
    }

    @Test
    fun testGroupDisbandMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testGroupDisbandNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testGroupDisbandAndRemoveMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testGroupDisbandAndRemoveNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testGroupDisbandForeignGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertNotEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertUnsuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_FAIL,
        )
    }

    @Test
    fun testDisbandForeignGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertNotEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertUnsuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testDisbandLeftGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialLeftGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(UserState.LEFT, groupModel.data?.userState)
        assertUnsuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_FAIL,
        )
    }

    @Test
    fun testDisbandLeftGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialLeftGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(UserState.LEFT, groupModel.data?.userState)
        assertUnsuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

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

        groupModelRepository.persistRemovedGroup(groupModel.groupIdentity)
        assertNull(groupModel.data)

        assertUnsuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

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

        groupModelRepository.persistRemovedGroup(groupModel.groupIdentity)
        assertNull(groupModel.data)

        assertUnsuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testDisbandNotesGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialNotesGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertEquals(emptySet(), groupModel.data?.otherMembers)
        assertEquals(UserState.MEMBER, groupModel.data?.userState)

        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testDisbandNotesGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialNotesGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertEquals(emptySet(), groupModel.data?.otherMembers)
        assertEquals(UserState.MEMBER, groupModel.data?.userState)

        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testDisbandAndRemoveNotesGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialNotesGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertEquals(emptySet(), groupModel.data?.otherMembers)
        assertEquals(UserState.MEMBER, groupModel.data?.userState)

        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testDisbandAndRemoveNotesGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialNotesGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertEquals(myContact.identity, groupModel.groupIdentity.creatorIdentity)
        assertEquals(emptySet(), groupModel.data?.otherMembers)
        assertEquals(UserState.MEMBER, groupModel.data?.userState)

        assertSuccessfulDisband(
            groupModel,
            GroupDisbandIntent.DISBAND_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun shouldNotDisbandGroupWhenMdActiveButConnectionIsLost() = runTest {
        // arrange
        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        val taskManager = ControlledTaskManager(emptyList())
        val groupFlowDispatcher = getGroupFlowDispatcher(
            setupConfig = SetupConfig.MULTI_DEVICE_ENABLED,
            taskManager = taskManager,
            connection = ConnectionDisconnected,
        )

        // act
        val groupFlowResult = groupFlowDispatcher
            .runDisbandGroupFlow(GroupDisbandIntent.DISBAND, groupModel!!)
            .await()

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

    private suspend fun assertSuccessfulDisband(
        groupModel: GroupModel,
        intent: GroupDisbandIntent,
        reflectionExpectation: ReflectionExpectation,
    ) {
        assertIs<GroupFlowResult.Success>(
            runGroupDisband(groupModel, intent, reflectionExpectation),
        )

        when (intent) {
            GroupDisbandIntent.DISBAND -> assertEquals(
                UserState.LEFT,
                groupModel.data?.userState,
            )

            GroupDisbandIntent.DISBAND_AND_REMOVE -> assertNull(groupModel.data)
        }
    }

    private suspend fun assertUnsuccessfulDisband(
        groupModel: GroupModel,
        intent: GroupDisbandIntent,
        reflectionExpectation: ReflectionExpectation,
    ) {
        val groupModelDataBefore = groupModel.data
        assertIs<GroupFlowResult.Failure>(
            runGroupDisband(groupModel, intent, reflectionExpectation),
        )
        val groupModelDataAfter = groupModel.data
        // Assert that the group model has not changed
        assertEquals(groupModelDataBefore, groupModelDataAfter)
    }

    private suspend fun runGroupDisband(
        groupModel: GroupModel,
        intent: GroupDisbandIntent,
        reflectionExpectation: ReflectionExpectation,
    ): GroupFlowResult {
        val groupModelData = groupModel.data

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

        // Run disband group flow
        val groupFlowResult = groupFlowDispatcher.runDisbandGroupFlow(
            intent,
            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?,
        intent: GroupDisbandIntent,
        reflectionExpectation: ReflectionExpectation,
    ): MutableList<(Task<*, TaskCodec>) -> Unit> {
        if (groupModelData == null ||
            groupModelData.groupIdentity.creatorIdentity != myContact.identity ||
            groupModelData.userState != UserState.MEMBER
        ) {
            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 ->
                when (intent) {
                    GroupDisbandIntent.DISBAND -> assertIs<ReflectLocalGroupLeaveOrDisband>(task)
                    GroupDisbandIntent.DISBAND_AND_REMOVE -> assertIs<ReflectGroupSyncDeleteTask>(
                        task,
                    )
                }
            }
        }

        // If the reflection fails, we do not expect a task that sends out csp messages
        if (reflectionExpectation != ReflectionExpectation.REFLECTION_FAIL) {
            scheduledTaskAssertions.add { task ->
                assertIs<OutgoingGroupDisbandTask>(task)
            }
        }

        return scheduledTaskAssertions
    }
}
