// Copyright 2018 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.feed;

import static org.mockito.AdditionalMatchers.or;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.tab.TabHidingType.CHANGED_TABS;
import static org.chromium.chrome.browser.tab.TabSelectionType.FROM_NEW;
import static org.chromium.chrome.browser.tab.TabSelectionType.FROM_USER;

import android.app.Activity;

import androidx.test.filters.SmallTest;

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

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabImpl;
import org.chromium.components.prefs.PrefService;

/**
 * Unit tests for {@link FeedSurfaceLifecycleManager}.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class NtpFeedSurfaceLifecycleManagerTest {
    @Mock
    private Activity mActivity;
    @Mock
    private TabImpl mTab;
    @Mock
    private Stream mStream;
    @Mock
    private PrefService mPrefService;
    @Mock
    private FeedSurfaceCoordinator mCoordinator;
    @Mock
    private FeedReliabilityLogger mFeedReliabilityLogger;

    private NtpFeedSurfaceLifecycleManager mNtpStreamLifecycleManager;

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

        // Initialize a test instance for PrefService.
        when(mPrefService.getBoolean(anyString())).thenReturn(true);
        doNothing().when(mPrefService).setBoolean(anyString(), anyBoolean());
        NtpFeedSurfaceLifecycleManager.setPrefServiceForTesting(mPrefService);
        when(mCoordinator.getFeedReliabilityLogger()).thenReturn(mFeedReliabilityLogger);

        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.CREATED);
        mNtpStreamLifecycleManager =
                new NtpFeedSurfaceLifecycleManager(mActivity, mTab, mCoordinator);
        verify(mStream, times(1)).onCreate(or(any(String.class), isNull()));
    }

    @Test
    @SmallTest
    public void testShow() {
        // Verify that onShow is not called before activity started.
        when((mTab).isHidden()).thenReturn(false);
        when(mTab.isUserInteractable()).thenReturn(true);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onShown(mTab, FROM_NEW);
        verify(mStream, times(0)).onShow();

        // Verify that onShow is not called when Tab is hidden.
        when((mTab).isHidden()).thenReturn(true);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        verify(mStream, times(0)).onShow();

        // Verify that onShow is called when Tab is shown and activity is started.
        when((mTab).isHidden()).thenReturn(false);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onShown(mTab, FROM_NEW);
        verify(mStream, times(1)).onShow();

        // When the Stream is shown, it won't call Stream#onShow() again.
        mNtpStreamLifecycleManager.getTabObserverForTesting().onShown(mTab, FROM_NEW);
        verify(mStream, times(1)).onShow();
    }

    @Test
    @SmallTest
    public void testShow_ArticlesNotVisible() {
        // Verify that onShow is not called when articles are set hidden by the user.
        when(mPrefService.getBoolean(Pref.ARTICLES_LIST_VISIBLE)).thenReturn(false);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        when((mTab).isHidden()).thenReturn(false);
        when(mTab.isUserInteractable()).thenReturn(true);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onShown(mTab, FROM_NEW);
        verify(mStream, times(0)).onShow();

        // Verify that onShow is called when articles are set shown by the user.
        when(mPrefService.getBoolean(Pref.ARTICLES_LIST_VISIBLE)).thenReturn(true);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onShown(mTab, FROM_NEW);
        verify(mStream, times(1)).onShow();

        // Verify that onHide is called after tab is hidden.
        mNtpStreamLifecycleManager.getTabObserverForTesting().onHidden(mTab, CHANGED_TABS);
        verify(mStream, times(1)).onHide();
    }

    @Test
    @SmallTest
    public void testHideFromActivityStopped() {
        // Activate the Stream.
        when((mTab).isHidden()).thenReturn(false);
        when(mTab.isUserInteractable()).thenReturn(true);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.RESUMED);
        verify(mStream, times(1)).onShow();

        // Deactivate the Stream.
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.PAUSED);

        // Verify that the Stream can be set hidden from inactive.
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STOPPED);
        verify(mStream, times(1)).onHide();
    }

    @Test
    @SmallTest
    public void testHideFromTabHiddenAfterShow() {
        // Show the stream.
        when((mTab).isHidden()).thenReturn(false);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        verify(mStream, times(1)).onShow();

        // Hide the stream.
        mNtpStreamLifecycleManager.getTabObserverForTesting().onHidden(
                mTab, TabHidingType.CHANGED_TABS);
        verify(mStream, times(1)).onShow();
        verify(mStream, times(1)).onHide();
    }

    @Test
    @SmallTest
    public void testDestroy() {
        // Verify that Stream#onDestroy is called on activity destroyed.
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.DESTROYED);
        verify(mStream, times(1)).onDestroy();
    }

    @Test
    @SmallTest
    public void testDestroyAfterCreate() {
        // After the Stream is destroyed, lifecycle methods should never be called. Directly calling
        // destroy here to simulate destroy() being called on FeedNewTabPage destroyed.
        mNtpStreamLifecycleManager.destroy();
        verify(mStream, times(1)).onDestroy();

        // Verify that lifecycle methods are not called after destroy.
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.RESUMED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.PAUSED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STOPPED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.DESTROYED);
        verify(mStream, times(0)).onShow();
        verify(mStream, times(0)).onHide();
        verify(mStream, times(1)).onDestroy();
    }

    @Test
    @SmallTest
    public void testDestroyAfterActivate() {
        InOrder inOrder = Mockito.inOrder(mStream);
        when((mTab).isHidden()).thenReturn(false);
        when(mTab.isUserInteractable()).thenReturn(true);

        // Activate the Stream.
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.RESUMED);
        inOrder.verify(mStream).onShow();
        verify(mStream, times(1)).onShow();

        // Verify that onInactive and onHide is called before onDestroy.
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.DESTROYED);
        inOrder.verify(mStream).onHide();
        inOrder.verify(mStream).onDestroy();
        verify(mStream, times(1)).onHide();
        verify(mStream, times(1)).onDestroy();
    }

    @Test
    @SmallTest
    public void testFullActivityLifecycle() {
        InOrder inOrder = Mockito.inOrder(mStream);
        when((mTab).isHidden()).thenReturn(false);
        when(mTab.isUserInteractable()).thenReturn(true);

        // On activity start and resume (simulates app become foreground).
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.RESUMED);
        inOrder.verify(mStream).onShow();
        verify(mStream, times(1)).onShow();

        // On activity pause and then resume (simulates multi-window mode).
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.PAUSED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.RESUMED);

        // On activity stop (simulates app switched to background).
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.PAUSED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STOPPED);
        inOrder.verify(mStream).onHide();
        verify(mStream, times(1)).onHide();

        // On activity start (simulates app switched back to foreground).
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.RESUMED);
        inOrder.verify(mStream).onShow();
        verify(mStream, times(2)).onShow();

        // On activity pause, stop, and destroy (simulates app removed from Android recents).
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.PAUSED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STOPPED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.DESTROYED);
        inOrder.verify(mStream).onHide();
        inOrder.verify(mStream).onDestroy();
        verify(mStream, times(2)).onHide();
        verify(mStream, times(1)).onDestroy();
    }

    @Test
    @SmallTest
    public void testFullTabLifecycle() {
        InOrder inOrder = Mockito.inOrder(mStream);

        // On new tab page created.
        when((mTab).isHidden()).thenReturn(true);
        when(mTab.isUserInteractable()).thenReturn(false);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.RESUMED);
        verify(mStream, times(0)).onShow();

        // On tab shown.
        when((mTab).isHidden()).thenReturn(false);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onShown(mTab, FROM_NEW);
        inOrder.verify(mStream).onShow();
        verify(mStream, times(1)).onShow();

        // On tab interactable.
        when(mTab.isUserInteractable()).thenReturn(true);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onInteractabilityChanged(mTab, true);

        // On tab un-interactable (simulates user enter the tab switcher).
        when(mTab.isUserInteractable()).thenReturn(false);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onInteractabilityChanged(mTab, false);

        // On tab interactable (simulates user exit the tab switcher).
        when(mTab.isUserInteractable()).thenReturn(true);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onInteractabilityChanged(mTab, true);

        // On tab un-interactable and hidden (simulates user switch to another tab).
        when((mTab).isHidden()).thenReturn(true);
        when(mTab.isUserInteractable()).thenReturn(false);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onInteractabilityChanged(mTab, false);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onHidden(mTab, CHANGED_TABS);
        inOrder.verify(mStream).onHide();
        verify(mStream, times(1)).onHide();

        // On tab shown (simulates user switch back to this tab).
        when((mTab).isHidden()).thenReturn(false);
        mNtpStreamLifecycleManager.getTabObserverForTesting().onShown(mTab, FROM_USER);
        inOrder.verify(mStream).onShow();
        verify(mStream, times(2)).onShow();

        // On tab destroy (simulates user close the tab or navigate to another URL).
        mNtpStreamLifecycleManager.destroy();
        inOrder.verify(mStream).onHide();
        inOrder.verify(mStream).onDestroy();
        verify(mStream, times(2)).onHide();
        verify(mStream, times(1)).onDestroy();
    }

    @Test
    @SmallTest
    public void testPaused() {
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.PAUSED);
        verify(mCoordinator).onActivityPaused();
    }
}
