/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.launcher3.model

import android.util.Pair
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.util.Executors
import com.android.launcher3.util.IntArray
import com.android.launcher3.util.TestUtil.runOnExecutorSync
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.times
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever

/** Tests for [AddWorkspaceItemsTask] */
@SmallTest
@RunWith(AndroidJUnit4::class)
class AddWorkspaceItemsTaskTest : AbstractWorkspaceModelTest() {

    private lateinit var mDataModelCallbacks: MyCallbacks

    private val mWorkspaceItemSpaceFinder: WorkspaceItemSpaceFinder = mock()

    @Before
    override fun setup() {
        super.setup()
        mDataModelCallbacks = MyCallbacks()
        Executors.MAIN_EXECUTOR.submit { mModelHelper.model.addCallbacks(mDataModelCallbacks) }
            .get()
    }

    @After
    override fun tearDown() {
        super.tearDown()
    }

    @Test
    fun givenNewItemAndNonEmptyPages_whenExecuteTask_thenAddNewItem() {
        val itemToAdd = getNewItem()
        val nonEmptyScreenIds = listOf(0, 1, 2)
        givenNewItemSpaces(NewItemSpace(1, 2, 2))

        val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd)

        assertThat(addedItems.size).isEqualTo(1)
        assertThat(addedItems.first().itemInfo.screenId).isEqualTo(1)
        assertThat(addedItems.first().isAnimated).isTrue()
        verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1)
    }

    @Test
    fun givenNewAndExistingItems_whenExecuteTask_thenOnlyAddNewItem() {
        val itemsToAdd = arrayOf(getNewItem(), getExistingItem())
        givenNewItemSpaces(NewItemSpace(1, 0, 0))
        val nonEmptyScreenIds = listOf(0)

        val addedItems = testAddItems(nonEmptyScreenIds, *itemsToAdd)

        assertThat(addedItems.size).isEqualTo(1)
        assertThat(addedItems.first().itemInfo.screenId).isEqualTo(1)
        assertThat(addedItems.first().isAnimated).isTrue()
        verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1)
    }

    @Test
    fun givenOnlyExistingItem_whenExecuteTask_thenDoNotAddItem() {
        val itemToAdd = getExistingItem()
        givenNewItemSpaces(NewItemSpace(1, 0, 0))
        val nonEmptyScreenIds = listOf(0)

        val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd)

        assertThat(addedItems.size).isEqualTo(0)
        // b/343530737
        verifyNoMoreInteractions(mWorkspaceItemSpaceFinder)
    }

    @Test
    fun givenNonSequentialScreenIds_whenExecuteTask_thenReturnNewScreenId() {
        val itemToAdd = getNewItem()
        givenNewItemSpaces(NewItemSpace(2, 1, 3))
        val nonEmptyScreenIds = listOf(0, 2, 3)

        val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd)

        assertThat(addedItems.size).isEqualTo(1)
        assertThat(addedItems.first().itemInfo.screenId).isEqualTo(2)
        assertThat(addedItems.first().isAnimated).isTrue()
        verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1)
    }

    @Test
    fun givenMultipleItems_whenExecuteTask_thenAddThem() {
        val itemsToAdd =
            arrayOf(getNewItem(), getExistingItem(), getNewItem(), getNewItem(), getExistingItem())
        givenNewItemSpaces(NewItemSpace(1, 3, 3), NewItemSpace(2, 0, 0), NewItemSpace(2, 0, 1))
        val nonEmptyScreenIds = listOf(0, 1)

        val addedItems = testAddItems(nonEmptyScreenIds, *itemsToAdd)

        // Only the new items should be added
        assertThat(addedItems.size).isEqualTo(3)

        // Items that are added to the first screen should not be animated
        val itemsAddedToFirstScreen = addedItems.filter { it.itemInfo.screenId == 1 }
        assertThat(itemsAddedToFirstScreen.size).isEqualTo(1)
        assertThat(itemsAddedToFirstScreen.first().isAnimated).isFalse()

        // Items that are added to the second screen should be animated
        val itemsAddedToSecondScreen = addedItems.filter { it.itemInfo.screenId == 2 }
        assertThat(itemsAddedToSecondScreen.size).isEqualTo(2)
        itemsAddedToSecondScreen.forEach { assertThat(it.isAnimated).isTrue() }
        verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 3)
    }

    /** Sets up the item space data that will be returned from WorkspaceItemSpaceFinder. */
    private fun givenNewItemSpaces(vararg newItemSpaces: NewItemSpace) {
        val spaceStack = newItemSpaces.toMutableList()
        whenever(mWorkspaceItemSpaceFinder.findSpaceForItem(any(), any(), any(), any())).then {
            spaceStack.removeFirst().toIntArray()
        }
    }

    /**
     * Verifies if WorkspaceItemSpaceFinder was called with proper arguments and how many times was
     * it called.
     */
    private fun verifyItemSpaceFinderCall(nonEmptyScreenIds: List<Int>, numberOfExpectedCall: Int) {
        verify(mWorkspaceItemSpaceFinder, times(numberOfExpectedCall))
            .findSpaceForItem(
                eq(IntArray.wrap(*nonEmptyScreenIds.toIntArray())),
                eq(IntArray()),
                eq(1),
                eq(1),
            )
    }

    /**
     * Sets up the workspaces with items, executes the task, collects the added items from the model
     * callback then returns it.
     */
    private fun testAddItems(
        nonEmptyScreenIds: List<Int>,
        vararg itemsToAdd: WorkspaceItemInfo,
    ): List<AddedItem> {
        setupWorkspaces(nonEmptyScreenIds)
        val task = newTask(*itemsToAdd)

        val addedItems = mutableListOf<AddedItem>()

        runOnExecutorSync(Executors.MODEL_EXECUTOR) {
            mDataModelCallbacks.addedItems.clear()
            mModelHelper.model.enqueueModelUpdateTask(task)
            runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
            addedItems.addAll(mDataModelCallbacks.addedItems)
        }

        return addedItems
    }

    /**
     * Creates the task with the given items and replaces the WorkspaceItemSpaceFinder dependency
     * with a mock.
     */
    private fun newTask(vararg items: ItemInfo): AddWorkspaceItemsTask =
        items
            .map { Pair.create(it, Any()) }
            .toMutableList()
            .let { AddWorkspaceItemsTask(it, mWorkspaceItemSpaceFinder) }
}

private data class AddedItem(val itemInfo: ItemInfo, val isAnimated: Boolean)

private class MyCallbacks : BgDataModel.Callbacks {

    val addedItems = mutableListOf<AddedItem>()

    override fun bindAppsAdded(
        newScreens: IntArray?,
        addNotAnimated: ArrayList<ItemInfo>,
        addAnimated: ArrayList<ItemInfo>,
    ) {
        addedItems.addAll(addAnimated.map { AddedItem(it, true) })
        addedItems.addAll(addNotAnimated.map { AddedItem(it, false) })
    }
}
