// Copyright 2019 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.app.tab_activity_glue;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;

import org.chromium.base.UserDataHost;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.app.tab_activity_glue.TabReparentingController.Delegate;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.AsyncTabParams;
import org.chromium.chrome.browser.tabmodel.AsyncTabParamsManager;
import org.chromium.chrome.browser.tabmodel.AsyncTabParamsManagerFactory;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabReparentingParams;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModel;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.util.HashMap;
import java.util.Map;

/**
 * Unit tests for {@link TabReparentingControllerTest}.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TabReparentingControllerTest {
    class FakeNightModeReparentingDelegate implements Delegate {
        TabModelSelector mTabModelSelector;

        @Override
        public TabModelSelector getTabModelSelector() {
            if (mTabModelSelector == null) {
                // setup
                mTabModelSelector = Mockito.mock(TabModelSelector.class);

                doReturn(mTabModel).when(mTabModelSelector).getModel(false);
                doReturn(mIncognitoTabModel).when(mTabModelSelector).getModel(true);
            }

            return mTabModelSelector;
        }

        @Override
        public boolean isNTPUrl(GURL url) {
            return UrlConstants.NTP_NON_NATIVE_URL.equals(url.getSpec())
                    || UrlConstants.NTP_URL.equals(url.getSpec());
        }
    }

    @Mock
    ReparentingTask mTask;

    MockTabModel mTabModel;
    MockTabModel mIncognitoTabModel;
    Map<Tab, Integer> mTabIndexMapping = new HashMap<>();
    Tab mForegroundTab;

    TabReparentingController mController;
    FakeNightModeReparentingDelegate mFakeDelegate;
    AsyncTabParamsManager mRealAsyncTabParamsManager;

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

        mTabModel = new MockTabModel(false, null);
        mIncognitoTabModel = new MockTabModel(true, null);

        mFakeDelegate = new FakeNightModeReparentingDelegate();
        mRealAsyncTabParamsManager = AsyncTabParamsManagerFactory.createAsyncTabParamsManager();
        mController = new TabReparentingController(mFakeDelegate, mRealAsyncTabParamsManager);
    }

    @After
    public void tearDown() {
        mForegroundTab = null;
        mRealAsyncTabParamsManager.getAsyncTabParams().clear();
        mTabIndexMapping.clear();
    }

    @Test
    public void testReparenting_singleTab() {
        mForegroundTab = createAndAddMockTab(1, false);
        mController.prepareTabsForReparenting();

        AsyncTabParams params = mRealAsyncTabParamsManager.getAsyncTabParams().get(1);
        Assert.assertNotNull(params);
        Assert.assertTrue(params instanceof TabReparentingParams);

        TabReparentingParams trp = (TabReparentingParams) params;
        Tab tab = trp.getTabToReparent();
        Assert.assertNotNull(tab);
        verify(mTask, times(1)).detach();
    }

    @Test
    public void testReparenting_singleTab_NTP() {
        // New tab pages aren't reparented intentionally.
        mForegroundTab =
                createAndAddMockTab(1, false, JUnitTestGURLs.getGURL(JUnitTestGURLs.NTP_URL));
        mController.prepareTabsForReparenting();

        Assert.assertFalse(mRealAsyncTabParamsManager.hasParamsWithTabToReparent());
    }

    @Test
    public void testReparenting_singleTab_reparentingAttemptedTwice() {
        mForegroundTab = createAndAddMockTab(1, false);
        mController.prepareTabsForReparenting();
        // Simulate the theme being changed twice before the application is recreated.
        mController.prepareTabsForReparenting();

        AsyncTabParams params = mRealAsyncTabParamsManager.getAsyncTabParams().get(1);
        Assert.assertNotNull(params);
        Assert.assertTrue(params instanceof TabReparentingParams);

        TabReparentingParams trp = (TabReparentingParams) params;
        Tab tab = trp.getTabToReparent();
        Assert.assertNotNull(tab);
        verify(mTask, times(1)).detach();
    }

    @Test
    public void testReparenting_multipleTabs() {
        mForegroundTab = createAndAddMockTab(1, false);
        createAndAddMockTab(2, false);
        mController.prepareTabsForReparenting();

        TabReparentingParams trp =
                (TabReparentingParams) mRealAsyncTabParamsManager.getAsyncTabParams().get(1);
        Tab tab = trp.getTabToReparent();
        Assert.assertNotNull(tab);
        trp = (TabReparentingParams) mRealAsyncTabParamsManager.getAsyncTabParams().get(2);

        tab = trp.getTabToReparent();
        Assert.assertNotNull(tab);

        verify(mTask, times(2)).detach();
    }

    @Test
    public void testReparenting_twoTabsOutOfOrder() {
        createAndAddMockTab(1, false);
        mForegroundTab = createAndAddMockTab(2, false);
        mController.prepareTabsForReparenting();

        AsyncTabParams params = mRealAsyncTabParamsManager.getAsyncTabParams().get(2);
        Assert.assertNotNull(params);
        Assert.assertTrue(params instanceof TabReparentingParams);

        TabReparentingParams trp = (TabReparentingParams) params;
        Tab tab = trp.getTabToReparent();
        Assert.assertNotNull(tab);

        verify(mTask, times(2)).detach();
    }

    @Test
    public void testReparenting_twoTabsOneIncognito() {
        createAndAddMockTab(1, false);
        mForegroundTab = createAndAddMockTab(2, true);
        mController.prepareTabsForReparenting();

        AsyncTabParams params = mRealAsyncTabParamsManager.getAsyncTabParams().get(2);
        Assert.assertNotNull(params);
        Assert.assertTrue(params instanceof TabReparentingParams);

        TabReparentingParams trp = (TabReparentingParams) params;

        Tab tab = trp.getTabToReparent();
        Assert.assertNotNull(tab);

        verify(mTask, times(2)).detach();
    }

    @Test
    public void testReparenting_threeTabsOutOfOrder() {
        createAndAddMockTab(3, false);
        mForegroundTab = createAndAddMockTab(2, false);
        createAndAddMockTab(1, false);
        mController.prepareTabsForReparenting();

        // Check the foreground tab.
        TabReparentingParams trp =
                (TabReparentingParams) mRealAsyncTabParamsManager.getAsyncTabParams().get(2);

        Tab tab = trp.getTabToReparent();
        Assert.assertNotNull(tab);

        // Check the background tabs.
        trp = (TabReparentingParams) mRealAsyncTabParamsManager.getAsyncTabParams().get(1);
        trp = (TabReparentingParams) mRealAsyncTabParamsManager.getAsyncTabParams().get(3);

        verify(mTask, times(3)).detach();
    }

    @Test
    public void testReparenting_stopLoadingIfNeeded() {
        // New tab pages aren't reparented intentionally.
        mForegroundTab = createAndAddMockTab(1, false);
        doReturn(true).when(mForegroundTab).isLoading();

        mController.prepareTabsForReparenting();

        verify(mForegroundTab).stopLoading();
        verify(mForegroundTab.getWebContents().getNavigationController()).setNeedsReload();
    }

    /**
     * Adds a tab to the correct model and sets the index in the mapping.
     *
     * @param id The id to give to the tab.
     * @param incognito Whether to add the tab to the incognito model or the regular model.
     * @param url The url to set for the tab.
     * @return The tab that was added. Use the return value to set the foreground tab for tests.
     */
    private Tab createAndAddMockTab(int id, boolean incognito, GURL url) {
        Tab tab = Mockito.mock(Tab.class);
        WebContents wc = Mockito.mock(WebContents.class);
        NavigationController nc = Mockito.mock(NavigationController.class);
        doReturn(url).when(tab).getUrl();
        doReturn(wc).when(tab).getWebContents();
        doReturn(nc).when(wc).getNavigationController();
        UserDataHost udh = new UserDataHost();
        udh.setUserData(ReparentingTask.class, mTask);
        doReturn(udh).when(tab).getUserDataHost();
        doReturn(id).when(tab).getId();

        int index;
        if (incognito) {
            mIncognitoTabModel.addTab(tab, -1, TabLaunchType.FROM_BROWSER_ACTIONS,
                    TabCreationState.LIVE_IN_FOREGROUND);
            index = mIncognitoTabModel.indexOf(tab);
        } else {
            mTabModel.addTab(tab, -1, TabLaunchType.FROM_BROWSER_ACTIONS,
                    TabCreationState.LIVE_IN_FOREGROUND);
            index = mTabModel.indexOf(tab);
        }
        mTabIndexMapping.put(tab, index);

        return tab;
    }

    private Tab createAndAddMockTab(int id, boolean incognito) {
        return createAndAddMockTab(
                id, incognito, JUnitTestGURLs.getGURL(JUnitTestGURLs.EXAMPLE_URL));
    }
}
