// 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.password_manager.settings;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import androidx.annotation.IntDef;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;

import org.chromium.base.test.BaseRobolectricTestRunner;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Tests for the {@link DialogManager} class.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@LooperMode(LooperMode.Mode.LEGACY)
public class DialogManagerTest {
    @Rule
    public MockitoRule mMockitoRule = MockitoJUnit.rule();

    /**
     * Fake of {@link DialogFragment} allowing to detect calls to {@link DialogFragment#show} and
     * {@link DialogFragment#dismiss}.
     */
    private static class FakeDialogFragment extends DialogFragment {
        @Retention(RetentionPolicy.SOURCE)
        @IntDef({NEW, SHOWING, DISMISSED})
        public @interface DialogState {}
        /** The dialog has not been shown yet. */
        public static final int NEW = 0;
        /** The dialog is showing. */
        public static final int SHOWING = 1;
        /** The dialog has been dismissed. */
        public static final int DISMISSED = 2;

        private @DialogState int mState = NEW;

        public @DialogState int getState() {
            return mState;
        }

        @Override
        public void show(FragmentManager manager, String tag) {
            assert mState == NEW;
            mState = SHOWING;
        }

        @Override
        public void dismiss() {
            assert mState == SHOWING;
            mState = DISMISSED;
        }
    }

    /** Used to detect showing and hiding without actually triggering any UI. */
    private final FakeDialogFragment mDialogFragment = new FakeDialogFragment();

    /** Used to test reporting from DialogManager. */
    private final DialogManager.ActionsConsumer mMockActionsConsumer =
            mock(DialogManager.ActionsConsumer.class);

    /** The object under test. */
    private final DialogManager mDialogManager = new DialogManager(mMockActionsConsumer);

    /**
     * Delayer to replace the timed one in the tested class. This gives exact control over hiding
     * delays.
     */
    private final ManualCallbackDelayer mManualDelayer = new ManualCallbackDelayer();

    @Before
    public void setUp() {
        mDialogManager.replaceCallbackDelayerForTesting(mManualDelayer);
    }

    /**
     * Check that a dialog is hidden eventually but not before a prescribed delay.
     */
    @Test
    public void testHiddenEventually() {
        mDialogManager.show(mDialogFragment, null);

        Runnable callback = mock(Runnable.class);
        mDialogManager.hide(callback);
        Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
        verify(callback, never()).run();

        mManualDelayer.runCallbacksSynchronously();
        verify(callback, times(1)).run();
        assertEquals(FakeDialogFragment.DISMISSED, mDialogFragment.getState());
        verify(mMockActionsConsumer, times(1)).consume(DialogManager.HideActions.HIDING_DELAYED);
    }

    /**
     * Check that immediate hiding is notified properly.
     */
    @Test
    public void testNotification() {
        mDialogManager.show(mDialogFragment, null);

        Runnable callback = mock(Runnable.class);
        mManualDelayer.runCallbacksSynchronously();
        mDialogManager.hide(callback);
        Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
        verify(callback, times(1)).run();
        assertEquals(FakeDialogFragment.DISMISSED, mDialogFragment.getState());
        verify(mMockActionsConsumer, times(1))
                .consume(DialogManager.HideActions.HIDDEN_IMMEDIATELY);
    }

    /**
     * Check that the callback is called even if the dialog has not been shown at all.
     */
    @Test
    public void testCallbackCalled() {
        Runnable callback = mock(Runnable.class);
        mDialogManager.hide(callback);
        Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
        verify(callback, times(1)).run();
        assertEquals(FakeDialogFragment.NEW, mDialogFragment.getState());
        verify(mMockActionsConsumer, times(1)).consume(DialogManager.HideActions.NO_OP);
    }
}
