// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.omnibox.suggestions;

import static org.chromium.components.omnibox.GroupConfigTestSupport.SECTION_1_NO_HEADER;
import static org.chromium.components.omnibox.GroupConfigTestSupport.SECTION_2_WITH_HEADER;
import static org.chromium.components.omnibox.GroupConfigTestSupport.SECTION_3_WITH_HEADER;
import static org.chromium.components.omnibox.GroupConfigTestSupport.SECTION_INVALID;

import android.util.ArraySet;

import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.AutocompleteMatchBuilder;
import org.chromium.components.omnibox.AutocompleteResult;
import org.chromium.components.omnibox.GroupsProto.GroupsInfo;
import org.chromium.components.omnibox.OmniboxSuggestionType;
import org.chromium.url.JUnitTestGURLs;
import org.chromium.url.ShadowGURL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * Unit tests for {@link CachedZeroSuggestionsManager}.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, shadows = {ShadowGURL.class})
public class CachedZeroSuggestionsManagerUnitTest {
    /**
     * Compare two instances of CachedZeroSuggestionsManager to see if they are same, asserting if
     * they're not. Note that order is just as relevant as the content for caching.
     */
    void assertAutocompleteResultEquals(AutocompleteResult data1, AutocompleteResult data2) {
        Assert.assertEquals(data1.getSuggestionsList(), data2.getSuggestionsList());
        Assert.assertEquals(data1.getGroupsInfo(), data2.getGroupsInfo());
    }

    /**
     * Build a dummy suggestions list.
     * @param count How many suggestions to create.
     * @param hasPostData If suggestions contain post data.
     *
     * @return List of suggestions.
     */
    private List<AutocompleteMatch> buildDummySuggestionsList(int count, boolean hasPostData) {
        List<AutocompleteMatch> list = new ArrayList<>();

        for (int index = 0; index < count; ++index) {
            final int id = index + 1;
            list.add(createSuggestionBuilder(id, OmniboxSuggestionType.HISTORY_URL)
                             .setPostContentType(hasPostData ? "Content Type " + id : null)
                             .setPostData(hasPostData ? new byte[] {4, 5, 6, (byte) id} : null)
                             .build());
        }

        return list;
    }

    /**
     * Create and partially initialize suggestion builder constructing dummy OmniboxSuggestions.
     *
     * @param id Suggestion identifier used to initialize a unique suggestion content.
     * @return Newly constructed AutocompleteMatch.
     */
    private AutocompleteMatchBuilder createSuggestionBuilder(int id) {
        return createSuggestionBuilder(id, OmniboxSuggestionType.HISTORY_URL);
    }

    /**
     * Create and partially initialize suggestion builder constructing dummy OmniboxSuggestions.
     *
     * @param id Suggestion identifier used to initialize a unique suggestion content.
     * @param type Suggestion type.
     * @return Newly constructed AutocompleteMatch.
     */
    private AutocompleteMatchBuilder createSuggestionBuilder(
            int id, @OmniboxSuggestionType int type) {
        return AutocompleteMatchBuilder.searchWithType(type)
                .setDisplayText("dummy text " + id)
                .setDescription("dummy description " + id);
    }

    @Test
    @SmallTest
    public void setNewSuggestions_cachedSuggestionsWithPostdataBeforeAndAfterAreSame() {
        AutocompleteResult dataToCache =
                AutocompleteResult.fromCache(buildDummySuggestionsList(2, true), null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToCache, dataFromCache);
    }

    @Test
    @SmallTest
    public void setNewSuggestions_cachedSuggestionsWithoutPostdataBeforeAndAfterAreSame() {
        AutocompleteResult dataToCache =
                AutocompleteResult.fromCache(buildDummySuggestionsList(2, false), null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToCache, dataFromCache);
    }

    @Test
    @SmallTest
    public void setNewSuggestions_DoNotcacheClipboardSuggestions() {
        List<AutocompleteMatch> mix_list = Arrays.asList(
                createSuggestionBuilder(1, OmniboxSuggestionType.CLIPBOARD_IMAGE).build(),
                createSuggestionBuilder(2, OmniboxSuggestionType.HISTORY_URL).build(),
                createSuggestionBuilder(3, OmniboxSuggestionType.CLIPBOARD_TEXT).build(),
                createSuggestionBuilder(4, OmniboxSuggestionType.SEARCH_HISTORY).build());
        List<AutocompleteMatch> expected_list =
                Arrays.asList(createSuggestionBuilder(2, OmniboxSuggestionType.HISTORY_URL).build(),
                        createSuggestionBuilder(4, OmniboxSuggestionType.SEARCH_HISTORY).build());

        AutocompleteResult dataToCache = AutocompleteResult.fromCache(mix_list, null);
        AutocompleteResult dataToExpected = AutocompleteResult.fromCache(expected_list, null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToExpected, dataFromCache);
    }

    @Test
    @SmallTest
    public void groupsDetails_restoreDetailsFromEmptyCache() {
        // Note: purge cache explicitly, because tests are run on an actual device
        // and cache may hold content from other test runs.
        AutocompleteResult dataToCache = AutocompleteResult.EMPTY_RESULT;
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToCache, dataFromCache);
    }

    @Test
    @SmallTest
    public void groupsDetails_cacheAllSaneGroupConfig() {
        var groupsDetails = GroupsInfo.newBuilder()
                                    .putGroupConfigs(10, SECTION_1_NO_HEADER)
                                    .putGroupConfigs(20, SECTION_2_WITH_HEADER)
                                    .putGroupConfigs(30, SECTION_3_WITH_HEADER)
                                    .build();

        AutocompleteResult dataToCache = AutocompleteResult.fromCache(null, groupsDetails);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToCache, dataFromCache);
    }

    @Test
    @SmallTest
    public void groupsDetails_restoreInvalidGroupsDetailsFromCache() {
        final SharedPreferencesManager manager = SharedPreferencesManager.getInstance();
        var groupsDetails = GroupsInfo.newBuilder()
                                    .putGroupConfigs(20, SECTION_2_WITH_HEADER)
                                    .putGroupConfigs(30, SECTION_1_NO_HEADER)
                                    .build();

        // Write to disk.
        AutocompleteResult dataToCache = AutocompleteResult.fromCache(null, groupsDetails);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);

        // Check that it works the first time (group details are correct).
        // Only the INVALID_GROUP should be removed.
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToCache, dataFromCache);

        // Truncate the data.
        var data = manager.readString(
                ChromePreferenceKeys.OMNIBOX_CACHED_ZERO_SUGGEST_GROUPS_INFO, null);
        data = data.substring(0, data.length() - 10);
        manager.writeString(ChromePreferenceKeys.OMNIBOX_CACHED_ZERO_SUGGEST_GROUPS_INFO, data);
        dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataFromCache, AutocompleteResult.EMPTY_RESULT);
        Assert.assertNull(manager.readString(
                ChromePreferenceKeys.OMNIBOX_CACHED_ZERO_SUGGEST_GROUPS_INFO, null));

        // Corrupt the data.
        manager.writeString(
                ChromePreferenceKeys.OMNIBOX_CACHED_ZERO_SUGGEST_GROUPS_INFO, "abcdefgh");
        dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataFromCache, AutocompleteResult.EMPTY_RESULT);
        Assert.assertNull(manager.readString(
                ChromePreferenceKeys.OMNIBOX_CACHED_ZERO_SUGGEST_GROUPS_INFO, null));

        // Remove the data.
        manager.removeKey(ChromePreferenceKeys.OMNIBOX_CACHED_ZERO_SUGGEST_GROUPS_INFO);
        dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataFromCache, AutocompleteResult.EMPTY_RESULT);
    }

    @Test
    @SmallTest
    public void dropSuggestions_suggestionsWithValidGroupsAssociation() {
        List<AutocompleteMatch> list = buildDummySuggestionsList(2, false);
        list.add(createSuggestionBuilder(33).setGroupId(1).build());

        var groupsDetails =
                GroupsInfo.newBuilder().putGroupConfigs(1, SECTION_2_WITH_HEADER).build();

        AutocompleteResult dataToCache = AutocompleteResult.fromCache(list, groupsDetails);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToCache, dataFromCache);
    }

    @Test
    @SmallTest
    public void dropSuggestions_suggestionsWithInvalidGroupsAssociation() {
        List<AutocompleteMatch> listExpected = buildDummySuggestionsList(2, false);
        List<AutocompleteMatch> listToCache = buildDummySuggestionsList(2, false);
        listToCache.add(createSuggestionBuilder(33).setGroupId(1).build());

        AutocompleteResult dataExpected = AutocompleteResult.fromCache(listExpected, null);
        AutocompleteResult dataToCache = AutocompleteResult.fromCache(listToCache, null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataExpected, dataFromCache);
    }

    @Test
    @SmallTest
    public void malformedCache_dropsMissingSuggestions() {
        // Clear cache explicitly, otherwise this test will be flaky until the suite is re-executed.
        ContextUtils.getAppSharedPreferences().edit().clear().apply();

        final SharedPreferencesManager manager = SharedPreferencesManager.getInstance();

        // Save one valid suggestion to cache.
        AutocompleteResult dataToCache =
                AutocompleteResult.fromCache(buildDummySuggestionsList(1, true), null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);

        // Signal that there's actually 2 items in the cache.
        manager.writeInt(ChromePreferenceKeys.KEY_ZERO_SUGGEST_LIST_SIZE, 2);

        // Construct an expected raw suggestion list content. This constitutes one valid entry
        // and 1 totally empty entry.
        AutocompleteResult rawDataFromCache =
                AutocompleteResult.fromCache(buildDummySuggestionsList(1, true), null);
        rawDataFromCache.getSuggestionsList().add(new AutocompleteMatchBuilder().build());

        // readCachedSuggestionList makes full attempt to restore whatever could be scraped from the
        // cache.
        List<AutocompleteMatch> readList =
                CachedZeroSuggestionsManager.readCachedSuggestionList(manager);
        Assert.assertEquals(2, readList.size());
        assertAutocompleteResultEquals(
                AutocompleteResult.fromCache(readList, null), rawDataFromCache);

        // Cache recovery however should be smart here and remove items that make no sense.
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataFromCache, dataToCache);
    }

    @Test
    @SmallTest
    public void removeInvalidSuggestions_dropsInvalidSuggestionsAndGroupsDetails() {
        // Write 3 wrong group groupsDetails to the cache
        var groupsDetailsExpected =
                GroupsInfo.newBuilder()
                        .putGroupConfigs(12, SECTION_2_WITH_HEADER)
                        .putGroupConfigs(AutocompleteMatch.INVALID_GROUP, SECTION_INVALID)
                        .build();
        var groupsDetailsWithInvalidItems =
                GroupsInfo.newBuilder()
                        .putGroupConfigs(12, SECTION_2_WITH_HEADER)
                        .putGroupConfigs(AutocompleteMatch.INVALID_GROUP, SECTION_INVALID)
                        .build();

        List<AutocompleteMatch> listExpected = buildDummySuggestionsList(2, false);
        listExpected.add(createSuggestionBuilder(72).setGroupId(12).build());

        List<AutocompleteMatch> listWithInvalidItems = buildDummySuggestionsList(2, false);
        listWithInvalidItems.add(createSuggestionBuilder(72).setGroupId(12).build());
        listWithInvalidItems.add(createSuggestionBuilder(73)
                                         .setGroupId(12)
                                         .setUrl(JUnitTestGURLs.getGURL(JUnitTestGURLs.INVALID_URL))
                                         .build());
        listWithInvalidItems.add(createSuggestionBuilder(74).setGroupId(34).build());

        AutocompleteResult dataWithInvalidItems =
                AutocompleteResult.fromCache(listWithInvalidItems, groupsDetailsWithInvalidItems);
        AutocompleteResult dataExpected =
                AutocompleteResult.fromCache(listExpected, groupsDetailsExpected);

        CachedZeroSuggestionsManager.removeInvalidSuggestionsAndGroupsDetails(
                dataWithInvalidItems.getSuggestionsList(),
                dataWithInvalidItems.getGroupsInfo().getGroupConfigsMap());
        assertAutocompleteResultEquals(dataWithInvalidItems, dataExpected);
    }

    @Test
    @SmallTest
    public void cacheAndRestoreSuggestionSubtypes() {
        List<AutocompleteMatch> list = Arrays.asList(
                createSuggestionBuilder(1, OmniboxSuggestionType.SEARCH_SUGGEST_PERSONALIZED)
                        .addSubtype(1)
                        .addSubtype(4)
                        .build(),
                createSuggestionBuilder(2, OmniboxSuggestionType.HISTORY_URL)
                        .addSubtype(17)
                        .build(),
                createSuggestionBuilder(3, OmniboxSuggestionType.SEARCH_SUGGEST_ENTITY)
                        .addSubtype(2)
                        .addSubtype(10)
                        .addSubtype(30)
                        .build(),
                createSuggestionBuilder(4, OmniboxSuggestionType.SEARCH_HISTORY).build());

        AutocompleteResult dataToCache = AutocompleteResult.fromCache(list, null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);
        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(dataToCache, dataFromCache);
    }

    @Test
    @SmallTest
    public void rejectCacheIfSubtypesAreMalformed() {
        List<AutocompleteMatch> list = Arrays.asList(
                createSuggestionBuilder(1, OmniboxSuggestionType.SEARCH_SUGGEST_PERSONALIZED)
                        .addSubtype(1)
                        .addSubtype(4)
                        .build(),
                createSuggestionBuilder(2, OmniboxSuggestionType.HISTORY_URL)
                        .addSubtype(17)
                        .build());

        AutocompleteResult dataToCache = AutocompleteResult.fromCache(list, null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);

        // Insert garbage for the Suggestion Subtypes.
        final SharedPreferencesManager manager = SharedPreferencesManager.getInstance();
        final Set<String> garbageSubtypes = new ArraySet<>();
        garbageSubtypes.add("invalid");
        manager.writeStringSet(
                ChromePreferenceKeys.KEY_ZERO_SUGGEST_NATIVE_SUBTYPES_PREFIX.createKey(1),
                garbageSubtypes);

        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(AutocompleteResult.EMPTY_RESULT, dataFromCache);
    }

    @Test
    @SmallTest
    public void rejectCacheIfSubtypesIncludeNull() {
        List<AutocompleteMatch> list = Arrays.asList(
                createSuggestionBuilder(1, OmniboxSuggestionType.SEARCH_SUGGEST_PERSONALIZED)
                        .addSubtype(1)
                        .build());

        AutocompleteResult dataToCache = AutocompleteResult.fromCache(list, null);
        CachedZeroSuggestionsManager.saveToCache(dataToCache);

        final SharedPreferencesManager manager = SharedPreferencesManager.getInstance();
        final Set<String> garbageSubtypes = new ArraySet<>();
        garbageSubtypes.add("null");
        manager.writeStringSet(
                ChromePreferenceKeys.KEY_ZERO_SUGGEST_NATIVE_SUBTYPES_PREFIX.createKey(0),
                garbageSubtypes);

        AutocompleteResult dataFromCache = CachedZeroSuggestionsManager.readFromCache();
        assertAutocompleteResultEquals(AutocompleteResult.EMPTY_RESULT, dataFromCache);
    }
}
