package ch.threema.app.groupmanagement

import ch.threema.app.DangerousTest
import ch.threema.app.groupflows.GroupFlowResult
import ch.threema.app.groupflows.GroupLeaveIntent
import ch.threema.app.tasks.OutgoingGroupLeaveTask
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.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
import kotlinx.coroutines.test.runTest

@DangerousTest
class LeaveGroupFlowTest : 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",
    )

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

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

    @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(initialLeftGroupModelData)
            createFromSync(initialKickedGroupModelData)
        }
    }

    @Test
    fun testGroupLeaveMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertSuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testGroupLeaveNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertSuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testGroupLeaveAndRemoveMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertSuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testGroupLeaveAndRemoveNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertSuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testGroupLeaveMyGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveMyGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testGroupLeaveAndRemoveMyGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveAndRemoveMyGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testLeaveLeftGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialLeftGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveLeftGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialLeftGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testLeaveAndRemoveLeftGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialLeftGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveAndRemoveLeftGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialLeftGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testLeaveKickedGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveKickedGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testLeaveAndRemoveKickedGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveAndRemoveKickedGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)
        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testLeaveRemovedGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)

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

        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveRemovedGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)

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

        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

    @Test
    fun testLeaveAndRemoveRemovedGroupMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)

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

        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SUCCESS,
        )
    }

    @Test
    fun testLeaveAndRemoveRemovedGroupNonMd() = runTest {
        val groupModel =
            groupModelRepository.getByGroupIdentity(initialKickedGroupModelData.groupIdentity)
        assertNotNull(groupModel)

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

        assertUnsuccessfulLeave(
            groupModel,
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            ReflectionExpectation.REFLECTION_SKIPPED,
        )
    }

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

        // act
        val groupFlowResult = groupFlowDispatcher
            .runLeaveGroupFlow(GroupLeaveIntent.LEAVE, groupModel!!)
            .await()

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

    private suspend fun assertSuccessfulLeave(
        groupModel: GroupModel,
        intent: GroupLeaveIntent,
        reflectionExpectation: ReflectionExpectation,
    ) {
        assertIs<GroupFlowResult.Success>(
            runGroupLeave(groupModel, intent, reflectionExpectation),
        )
        when (intent) {
            GroupLeaveIntent.LEAVE -> assertEquals(UserState.LEFT, groupModel.data?.userState)
            GroupLeaveIntent.LEAVE_AND_REMOVE -> assertNull(groupModel.data)
        }
    }

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

    private suspend fun runGroupLeave(
        groupModel: GroupModel,
        intent: GroupLeaveIntent,
        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 leave group flow
        val groupFlowResult = groupFlowDispatcher.runLeaveGroupFlow(
            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: GroupLeaveIntent,
        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) {
                    GroupLeaveIntent.LEAVE -> assertIs<ReflectLocalGroupLeaveOrDisband>(task)
                    GroupLeaveIntent.LEAVE_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<OutgoingGroupLeaveTask>(task)
            }
        }

        return scheduledTaskAssertions
    }
}
