// Copyright 2014 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.dom_distiller;

import androidx.test.filters.SmallTest;

import com.google.common.util.concurrent.AtomicDouble;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.UiThreadTest;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.test.ChromeBrowserTestRule;
import org.chromium.components.dom_distiller.core.DistilledPagePrefs;
import org.chromium.components.dom_distiller.core.DomDistillerService;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.dom_distiller.mojom.FontFamily;
import org.chromium.dom_distiller.mojom.Theme;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Test class for {@link DistilledPagePrefs}.
 */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class DistilledPagePrefsTest {
    @ClassRule
    public static final ChromeBrowserTestRule sChromeBrowserTestRule = new ChromeBrowserTestRule();

    private DistilledPagePrefs mDistilledPagePrefs;

    private static final double EPSILON = 1e-5;

    private static final TimeUnit SEMAPHORE_TIMEOUT_UNIT = TimeUnit.SECONDS;
    private static final long SEMAPHORE_TIMEOUT_VALUE = 5;

    @Before
    public void setUp() {
        getDistilledPagePrefs();
    }

    @After
    public void tearDown() {
        // Set back to default theme
        setTheme(Theme.LIGHT);
    }

    private void getDistilledPagePrefs() {
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            // TODO (https://crbug.com/1063807):  Add incognito mode tests.
            DomDistillerService domDistillerService =
                    DomDistillerServiceFactory.getForProfile(Profile.getLastUsedRegularProfile());
            mDistilledPagePrefs = domDistillerService.getDistilledPagePrefs();
        });
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"DomDistiller"})
    public void testGetAndSetTheme() {
        // Check the default theme.
        Assert.assertEquals(Theme.LIGHT, mDistilledPagePrefs.getTheme());
        // Check that theme can be correctly set.
        setTheme(Theme.DARK);
        Assert.assertEquals(Theme.DARK, mDistilledPagePrefs.getTheme());
        setTheme(Theme.LIGHT);
        Assert.assertEquals(Theme.LIGHT, mDistilledPagePrefs.getTheme());
        setTheme(Theme.SEPIA);
        Assert.assertEquals(Theme.SEPIA, mDistilledPagePrefs.getTheme());
    }

    @Test
    @SmallTest
    @Feature({"DomDistiller"})
    public void testSingleObserverTheme() throws InterruptedException {
        TestingObserver testObserver = new TestingObserver();
        addObserver(testObserver);

        Assert.assertEquals(Theme.LIGHT, testObserver.getTheme());
        setTheme(Theme.DARK);
        // Check that testObserver's theme has been updated,
        Assert.assertEquals(Theme.DARK, testObserver.getThemeAfterWaiting());
        removeObserver(testObserver);
    }

    @Test
    @SmallTest
    @Feature({"DomDistiller"})
    public void testMultipleObserversTheme() throws InterruptedException {
        TestingObserver testObserverOne = new TestingObserver();
        addObserver(testObserverOne);
        TestingObserver testObserverTwo = new TestingObserver();
        addObserver(testObserverTwo);

        setTheme(Theme.SEPIA);
        Assert.assertEquals(Theme.SEPIA, testObserverOne.getThemeAfterWaiting());
        Assert.assertEquals(Theme.SEPIA, testObserverTwo.getThemeAfterWaiting());
        removeObserver(testObserverOne);

        setTheme(Theme.DARK);
        // Check that testObserverOne's theme is not changed but testObserverTwo's is.
        Assert.assertEquals(Theme.DARK, testObserverTwo.getThemeAfterWaiting());
        // There is no simple way to safely wait for something not to happen unless we force a timed
        // wait, which would slow down test runs. Since testObserverTwo has been invoked, we
        // incorrectly assume that testObserverOne would have been as well to keep the test runtime
        // short.
        Assert.assertEquals(Theme.SEPIA, testObserverOne.getTheme());
        removeObserver(testObserverTwo);
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"DomDistiller"})
    public void testGetAndSetFontFamily() {
        // Check the default font family.
        Assert.assertEquals(FontFamily.SANS_SERIF, mDistilledPagePrefs.getFontFamily());
        // Check that font family can be correctly set.
        setFontFamily(FontFamily.SERIF);
        Assert.assertEquals(FontFamily.SERIF, mDistilledPagePrefs.getFontFamily());
    }

    @Test
    @SmallTest
    @Feature({"DomDistiller"})
    public void testSingleObserverFontFamily() throws InterruptedException {
        TestingObserver testObserver = new TestingObserver();
        addObserver(testObserver);

        Assert.assertEquals(FontFamily.SANS_SERIF, testObserver.getFontFamily());
        setFontFamily(FontFamily.SERIF);
        // Check that testObserver's font family has been updated.
        Assert.assertEquals(FontFamily.SERIF, testObserver.getFontFamilyAfterWaiting());
        removeObserver(testObserver);
    }

    @Test
    @SmallTest
    @Feature({"DomDistiller"})
    public void testMultipleObserversFontFamily() throws InterruptedException {
        TestingObserver testObserverOne = new TestingObserver();
        addObserver(testObserverOne);
        TestingObserver testObserverTwo = new TestingObserver();
        addObserver(testObserverTwo);

        setFontFamily(FontFamily.MONOSPACE);
        Assert.assertEquals(FontFamily.MONOSPACE, testObserverOne.getFontFamilyAfterWaiting());
        Assert.assertEquals(FontFamily.MONOSPACE, testObserverTwo.getFontFamilyAfterWaiting());
        removeObserver(testObserverOne);

        setFontFamily(FontFamily.SERIF);
        // Check that testObserverOne's font family is not changed but testObserverTwo's is.
        Assert.assertEquals(FontFamily.SERIF, testObserverTwo.getFontFamilyAfterWaiting());
        // There is no simple way to safely wait for something not to happen unless we force a timed
        // wait, which would slow down test runs. Since testObserverTwo has been invoked, we
        // incorrectly assume that testObserverOne would have been as well to keep the test runtime
        // short.
        Assert.assertEquals(FontFamily.MONOSPACE, testObserverOne.getFontFamily());
        removeObserver(testObserverTwo);
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"DomDistiller"})
    public void testGetAndSetFontScaling() {
        // Check the default font scaling.
        Assert.assertEquals(1.0, mDistilledPagePrefs.getFontScaling(), EPSILON);
        // Check that font scaling can be correctly set.
        setFontScaling(1.2f);
        Assert.assertEquals(1.2, mDistilledPagePrefs.getFontScaling(), EPSILON);
    }

    @Test
    @SmallTest
    @Feature({"DomDistiller"})
    public void testSingleObserverFontScaling() throws InterruptedException {
        TestingObserver testObserver = new TestingObserver();
        addObserver(testObserver);

        Assert.assertNotEquals(1.1, testObserver.getFontScaling(), EPSILON);
        setFontScaling(1.1f);
        // Check that testObserver's font scaling has been updated.
        Assert.assertEquals(1.1, testObserver.getFontScalingAfterWaiting(), EPSILON);
        removeObserver(testObserver);
    }

    @Test
    @SmallTest
    @Feature({"DomDistiller"})
    public void testMultipleObserversFontScaling() throws InterruptedException {
        TestingObserver testObserverOne = new TestingObserver();
        addObserver(testObserverOne);
        TestingObserver testObserverTwo = new TestingObserver();
        addObserver(testObserverTwo);

        setFontScaling(1.3f);
        Assert.assertEquals(1.3, testObserverOne.getFontScalingAfterWaiting(), EPSILON);
        Assert.assertEquals(1.3, testObserverTwo.getFontScalingAfterWaiting(), EPSILON);
        removeObserver(testObserverOne);

        setFontScaling(0.9f);
        // Check that testObserverOne's font scaling is not changed but testObserverTwo's is.
        Assert.assertEquals(0.9, testObserverTwo.getFontScalingAfterWaiting(), EPSILON);
        // There is no simple way to safely wait for something not to happen unless we force a timed
        // wait, which would slow down test runs. Since testObserverTwo has been invoked, we
        // incorrectly assume that testObserverOne would have been as well to keep the test runtime
        // short.
        Assert.assertEquals(1.3, testObserverOne.getFontScaling(), EPSILON);
        removeObserver(testObserverTwo);
    }

    @Test
    @SmallTest
    @Feature({"DomDistiller"})
    public void testRepeatedAddAndDeleteObserver() {
        TestingObserver test = new TestingObserver();

        // Should successfully add the observer the first time.
        Assert.assertTrue(addObserver(test));
        // Observer cannot be added again, should return false.
        Assert.assertFalse(addObserver(test));

        // Delete the observer the first time.
        Assert.assertTrue(removeObserver(test));
        // Observer cannot be deleted again, should return false.
        Assert.assertFalse(removeObserver(test));
    }

    private static class TestingObserver implements DistilledPagePrefs.Observer {
        private final AtomicInteger mFontFamily = new AtomicInteger();
        private Semaphore mFontFamilySemaphore = new Semaphore(0);
        private final AtomicInteger mTheme = new AtomicInteger();
        private Semaphore mThemeSemaphore = new Semaphore(0);
        private final AtomicDouble mFontScaling = new AtomicDouble();
        private Semaphore mFontScalingSemaphore = new Semaphore(0);

        public TestingObserver() {}

        public int getFontFamily() {
            return mFontFamily.get();
        }

        public int getFontFamilyAfterWaiting() throws InterruptedException {
            Assert.assertTrue("Did not receive an update for font family",
                    mFontFamilySemaphore.tryAcquire(
                            SEMAPHORE_TIMEOUT_VALUE, SEMAPHORE_TIMEOUT_UNIT));
            return getFontFamily();
        }

        @Override
        public void onChangeFontFamily(int font) {
            mFontFamily.set(font);
            mFontFamilySemaphore.release();
        }

        public int getTheme() {
            return mTheme.get();
        }

        public int getThemeAfterWaiting() throws InterruptedException {
            Assert.assertTrue("Did not receive an update for theme",
                    mThemeSemaphore.tryAcquire(SEMAPHORE_TIMEOUT_VALUE, SEMAPHORE_TIMEOUT_UNIT));
            return getTheme();
        }

        @Override
        public void onChangeTheme(int theme) {
            mTheme.set(theme);
            mThemeSemaphore.release();
        }

        public float getFontScaling() {
            return (float) mFontScaling.get();
        }

        public float getFontScalingAfterWaiting() throws InterruptedException {
            Assert.assertTrue("Did not receive an update for font scaling",
                    mFontScalingSemaphore.tryAcquire(
                            SEMAPHORE_TIMEOUT_VALUE, SEMAPHORE_TIMEOUT_UNIT));
            return getFontScaling();
        }

        @Override
        public void onChangeFontScaling(float scaling) {
            mFontScaling.set(scaling);
            mFontScalingSemaphore.release();
        }
    }

    private void setFontFamily(final int font) {
        TestThreadUtils.runOnUiThreadBlocking(() -> mDistilledPagePrefs.setFontFamily(font));
    }

    private void setTheme(final int theme) {
        TestThreadUtils.runOnUiThreadBlocking(() -> mDistilledPagePrefs.setTheme(theme));
    }

    private void setFontScaling(final float scaling) {
        TestThreadUtils.runOnUiThreadBlocking(() -> mDistilledPagePrefs.setFontScaling(scaling));
    }

    private boolean removeObserver(TestingObserver testObserver) {
        AtomicBoolean wasRemoved = new AtomicBoolean();
        TestThreadUtils.runOnUiThreadBlocking(
                () -> wasRemoved.set(mDistilledPagePrefs.removeObserver(testObserver)));
        return wasRemoved.get();
    }

    private boolean addObserver(TestingObserver testObserver) {
        AtomicBoolean wasAdded = new AtomicBoolean();
        TestThreadUtils.runOnUiThreadBlocking(
                () -> wasAdded.set(mDistilledPagePrefs.addObserver(testObserver)));
        return wasAdded.get();
    }
}
