/*
 * Copyright (C) 2016 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.settings.dashboard;

import static com.android.settings.dashboard.DashboardData.STABLE_ID_CONDITION_CONTAINER;
import static com.android.settings.dashboard.DashboardData.STABLE_ID_CONDITION_FOOTER;
import static com.android.settings.dashboard.DashboardData.STABLE_ID_SUGGESTION_CONDITION_DIVIDER;
import static com.android.settings.dashboard.DashboardData.STABLE_ID_SUGGESTION_CONTAINER;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.app.PendingIntent;
import android.service.settings.suggestions.Suggestion;
import android.support.annotation.NonNull;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;

import com.android.settings.dashboard.conditional.AirplaneModeCondition;
import com.android.settings.dashboard.conditional.Condition;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

@RunWith(RobolectricTestRunner.class)
public class DashboardDataTest {

    private static final String TEST_SUGGESTION_TITLE = "Use fingerprint";
    private static final String TEST_CATEGORY_TILE_TITLE = "Display";

    private DashboardData mDashboardDataWithOneConditions;
    private DashboardData mDashboardDataWithTwoConditions;
    private DashboardData mDashboardDataWithNoItems;
    private DashboardCategory mDashboardCategory;
    @Mock
    private Tile mTestCategoryTile;
    @Mock
    private Condition mTestCondition;
    @Mock
    private Condition mSecondCondition; // condition used to test insert in DiffUtil
    private Suggestion mTestSuggestion;

    @Before
    public void SetUp() {
        MockitoAnnotations.initMocks(this);

        mDashboardCategory = new DashboardCategory();

        // Build suggestions
        final List<Suggestion> suggestions = new ArrayList<>();
        mTestSuggestion = new Suggestion.Builder("pkg")
                .setTitle(TEST_SUGGESTION_TITLE)
                .setPendingIntent(mock(PendingIntent.class))
                .build();
        suggestions.add(mTestSuggestion);

        // Build oneItemConditions
        final List<Condition> oneItemConditions = new ArrayList<>();
        when(mTestCondition.shouldShow()).thenReturn(true);
        oneItemConditions.add(mTestCondition);

        // Build twoItemConditions
        final List<Condition> twoItemsConditions = new ArrayList<>();
        when(mSecondCondition.shouldShow()).thenReturn(true);
        twoItemsConditions.add(mTestCondition);
        twoItemsConditions.add(mSecondCondition);

        // Build category
        mTestCategoryTile.title = TEST_CATEGORY_TILE_TITLE;
        mDashboardCategory.title = "test";

        mDashboardCategory.addTile(mTestCategoryTile);

        // Build DashboardData
        mDashboardDataWithOneConditions = new DashboardData.Builder()
                .setConditions(oneItemConditions)
                .setCategory(mDashboardCategory)
                .setSuggestions(suggestions)
                .setConditionExpanded(true)
                .build();

        mDashboardDataWithTwoConditions = new DashboardData.Builder()
                .setConditions(twoItemsConditions)
                .setCategory(mDashboardCategory)
                .setSuggestions(suggestions)
                .setConditionExpanded(true)
                .build();

        mDashboardDataWithNoItems = new DashboardData.Builder()
                .setConditions(null)
                .setCategory(null)
                .setSuggestions(null)
                .build();
    }

    @Test
    public void testBuildItemsData_shouldSetstableId() {
        final List<DashboardData.Item> items = mDashboardDataWithOneConditions.getItemList();

        // suggestion, separator, condition, footer, 1 tile
        assertThat(items).hasSize(5);

        assertThat(items.get(0).id).isEqualTo(STABLE_ID_SUGGESTION_CONTAINER);
        assertThat(items.get(1).id).isEqualTo(STABLE_ID_SUGGESTION_CONDITION_DIVIDER);
        assertThat(items.get(2).id).isEqualTo(STABLE_ID_CONDITION_CONTAINER);
        assertThat(items.get(3).id).isEqualTo(STABLE_ID_CONDITION_FOOTER);
        assertThat(items.get(4).id).isEqualTo(Objects.hash(mTestCategoryTile.title));
    }

    @Test
    public void testBuildItemsData_containsAllData() {
        final Object[] expectedObjects = {
                mDashboardDataWithOneConditions.getSuggestions(),
                null /* divider */,
                mDashboardDataWithOneConditions.getConditions(),
                null /* footer */, mTestCategoryTile};
        final int expectedSize = expectedObjects.length;

        assertThat(mDashboardDataWithOneConditions.getItemList()).hasSize(expectedSize);

        for (int i = 0; i < expectedSize; i++) {
            final Object item = mDashboardDataWithOneConditions.getItemEntityByPosition(i);
            if (item instanceof List) {
                assertThat(item).isEqualTo(expectedObjects[i]);
            } else if (item instanceof DashboardData.ConditionHeaderData) {
                DashboardData.ConditionHeaderData i1 = (DashboardData.ConditionHeaderData) item;
                DashboardData.ConditionHeaderData i2 =
                        (DashboardData.ConditionHeaderData) expectedObjects[i];
                assertThat(i1.title).isEqualTo(i2.title);
                assertThat(i1.conditionCount).isEqualTo(i2.conditionCount);
            } else {
                assertThat(item).isSameAs(expectedObjects[i]);
            }
        }
    }

    @Test
    public void testGetPositionByEntity_selfInstance_returnPositionFound() {
        final int position = mDashboardDataWithOneConditions
                .getPositionByEntity(mDashboardDataWithOneConditions.getConditions());
        assertThat(position).isNotEqualTo(DashboardData.POSITION_NOT_FOUND);
    }

    @Test
    public void testGetPositionByEntity_notExisted_returnNotFound() {
        final Condition condition = mock(AirplaneModeCondition.class);
        final int position = mDashboardDataWithOneConditions.getPositionByEntity(condition);
        assertThat(position).isEqualTo(DashboardData.POSITION_NOT_FOUND);
    }

    @Test
    public void testGetPositionByTile_selfInstance_returnPositionFound() {
        final int position = mDashboardDataWithOneConditions.getPositionByTile(mTestCategoryTile);
        assertThat(position).isNotEqualTo(DashboardData.POSITION_NOT_FOUND);
    }

    @Test
    public void testGetPositionByTile_equalTitle_returnPositionFound() {
        final Tile tile = mock(Tile.class);
        tile.title = TEST_CATEGORY_TILE_TITLE;
        final int position = mDashboardDataWithOneConditions.getPositionByTile(tile);
        assertThat(position).isNotEqualTo(DashboardData.POSITION_NOT_FOUND);
    }

    @Test
    public void testGetPositionByTile_notExisted_returnNotFound() {
        final Tile tile = mock(Tile.class);
        tile.title = "";
        final int position = mDashboardDataWithOneConditions.getPositionByTile(tile);
        assertThat(position).isEqualTo(DashboardData.POSITION_NOT_FOUND);
    }

    @Test
    public void testDiffUtil_DataEqual_noResultData() {
        List<ListUpdateResult.ResultData> testResultData = new ArrayList<>();
        testDiffUtil(mDashboardDataWithOneConditions,
                mDashboardDataWithOneConditions, testResultData);
    }

    @Test
    public void testDiffUtil_InsertOneCondition_ResultDataOneChanged() {
        final List<ListUpdateResult.ResultData> testResultData = new ArrayList<>();
        // Item in position 3 is the condition container containing the list of conditions, which
        // gets 1 more item
        testResultData.add(new ListUpdateResult.ResultData(
                ListUpdateResult.ResultData.TYPE_OPERATION_CHANGE, 2, 1));

        testDiffUtil(mDashboardDataWithOneConditions,
                mDashboardDataWithTwoConditions, testResultData);
    }

    @Test
    public void testDiffUtil_RemoveOneSuggestion_causeItemRemoveAndChange() {
        final List<ListUpdateResult.ResultData> testResultData = new ArrayList<>();
        // removed suggestion and the divider
        testResultData.add(new ListUpdateResult.ResultData(
                ListUpdateResult.ResultData.TYPE_OPERATION_REMOVE, 0, 2));
        testResultData.add(new ListUpdateResult.ResultData(
                ListUpdateResult.ResultData.TYPE_OPERATION_CHANGE, 2, 1));
        // Build DashboardData
        final List<Condition> oneItemConditions = new ArrayList<>();
        when(mTestCondition.shouldShow()).thenReturn(true);
        oneItemConditions.add(mTestCondition);
        final List<Suggestion> suggestions = new ArrayList<>();
        suggestions.add(mTestSuggestion);

        final DashboardData oldData = new DashboardData.Builder()
                .setConditions(oneItemConditions)
                .setCategory(mDashboardCategory)
                .setSuggestions(suggestions)
                .setConditionExpanded(false)
                .build();
        final DashboardData newData = new DashboardData.Builder()
                .setConditions(oneItemConditions)
                .setSuggestions(null)
                .setCategory(mDashboardCategory)
                .setConditionExpanded(false)
                .build();

        testDiffUtil(oldData, newData, testResultData);
    }

    @Test
    public void testDiffUtil_DeleteAllData_ResultDataOneDeleted() {
        final List<ListUpdateResult.ResultData> testResultData = new ArrayList<>();
        testResultData.add(new ListUpdateResult.ResultData(
                ListUpdateResult.ResultData.TYPE_OPERATION_REMOVE, 0, 5));

        testDiffUtil(mDashboardDataWithOneConditions, mDashboardDataWithNoItems, testResultData);
    }

    @Test
    public void testDiffUtil_typeSuggestedContainer_ResultDataNothingChanged() {
        final List<ListUpdateResult.ResultData> testResultData = new ArrayList<>();

        DashboardData prevData = new DashboardData.Builder()
                .setConditions(null)
                .setCategory(null)
                .setSuggestions(Collections.singletonList(mTestSuggestion))
                .build();
        DashboardData currentData = new DashboardData.Builder()
                .setConditions(null)
                .setCategory(null)
                .setSuggestions(Collections.singletonList(mTestSuggestion))
                .build();
        testDiffUtil(prevData, currentData, testResultData);
    }

    /**
     * Test when using the
     * {@link com.android.settings.dashboard.DashboardData.ItemsDataDiffCallback}
     * to transfer List from {@paramref baseDashboardData} to {@paramref diffDashboardData}, whether
     * the transform data result is equals to {@paramref testResultData}
     * <p>
     * The steps are described below:
     * 1. Calculate a {@link android.support.v7.util.DiffUtil.DiffResult} from
     * {@paramref baseDashboardData} to {@paramref diffDashboardData}
     * <p>
     * 2. Dispatch the {@link android.support.v7.util.DiffUtil.DiffResult} calculated from step 1
     * into {@link ListUpdateResult}
     * <p>
     * 3. Get result data(a.k.a. baseResultData) from {@link ListUpdateResult} and compare it to
     * {@paramref testResultData}
     * <p>
     * Because baseResultData and {@paramref testResultData} don't have sequence. When do the
     * comparison, we will sort them first and then compare the inside data from them one by one.
     */
    private void testDiffUtil(DashboardData baseDashboardData, DashboardData diffDashboardData,
            List<ListUpdateResult.ResultData> testResultData) {
        final DiffUtil.DiffResult diffUtilResult = DiffUtil.calculateDiff(
                new DashboardData.ItemsDataDiffCallback(
                        baseDashboardData.getItemList(), diffDashboardData.getItemList()));

        // Dispatch to listUpdateResult, then listUpdateResult will have result data
        final ListUpdateResult listUpdateResult = new ListUpdateResult();
        diffUtilResult.dispatchUpdatesTo(listUpdateResult);

        final List<ListUpdateResult.ResultData> baseResultData = listUpdateResult.getResultData();
        assertThat(testResultData.size()).isEqualTo(baseResultData.size());

        // Sort them so we can compare them one by one using a for loop
        Collections.sort(baseResultData);
        Collections.sort(testResultData);
        final int size = baseResultData.size();
        for (int i = 0; i < size; i++) {
            // Refer to equals method in ResultData
            assertThat(baseResultData.get(i)).isEqualTo(testResultData.get(i));
        }
    }

    /**
     * This class contains the result about how the changes made to convert one
     * list to another list. It implements ListUpdateCallback to record the result data.
     */
    private static class ListUpdateResult implements ListUpdateCallback {
        final private List<ResultData> mResultData;

        public ListUpdateResult() {
            mResultData = new ArrayList<>();
        }

        private List<ResultData> getResultData() {
            return mResultData;
        }

        @Override
        public void onInserted(int position, int count) {
            mResultData.add(new ResultData(ResultData.TYPE_OPERATION_INSERT, position, count));
        }

        @Override
        public void onRemoved(int position, int count) {
            mResultData.add(new ResultData(ResultData.TYPE_OPERATION_REMOVE, position, count));
        }

        @Override
        public void onMoved(int fromPosition, int toPosition) {
            mResultData.add(
                    new ResultData(ResultData.TYPE_OPERATION_MOVE, fromPosition, toPosition));
        }

        @Override
        public void onChanged(int position, int count, Object payload) {
            mResultData.add(new ResultData(ResultData.TYPE_OPERATION_CHANGE, position, count));
        }

        /**
         * This class contains general type and field to record the operation data generated
         * in {@link ListUpdateCallback}. Please refer to {@link ListUpdateCallback} for more info.
         * <p>
         * The following are examples about the data stored in this class:
         * <p>
         * "The data starts from position(arg1) with count number(arg2) is changed(operation)"
         * or "The data is moved(operation) from position1(arg1) to position2(arg2)"
         */
        private static class ResultData implements Comparable<ResultData> {

            private static final int TYPE_OPERATION_INSERT = 0;
            private static final int TYPE_OPERATION_REMOVE = 1;
            private static final int TYPE_OPERATION_MOVE = 2;
            private static final int TYPE_OPERATION_CHANGE = 3;

            private final int operation;
            private final int arg1;
            private final int arg2;

            private ResultData(int operation, int arg1, int arg2) {
                this.operation = operation;
                this.arg1 = arg1;
                this.arg2 = arg2;
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }

                if (!(obj instanceof ResultData)) {
                    return false;
                }

                ResultData targetData = (ResultData) obj;

                return operation == targetData.operation && arg1 == targetData.arg1
                        && arg2 == targetData.arg2;
            }

            @Override
            public int compareTo(@NonNull ResultData resultData) {
                if (this.operation != resultData.operation) {
                    return operation - resultData.operation;
                }

                if (arg1 != resultData.arg1) {
                    return arg1 - resultData.arg1;
                }

                return arg2 - resultData.arg2;
            }

            @Override
            public String toString() {
                return "op:" + operation + ",arg1:" + arg1 + ",arg2:" + arg2;
            }
        }
    }
}
