chromium/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataTest.java

// 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.tab.state;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import androidx.annotation.Nullable;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.optimization_guide.OptimizationGuideBridge;
import org.chromium.chrome.browser.optimization_guide.OptimizationGuideBridgeFactory;
import org.chromium.chrome.browser.optimization_guide.OptimizationGuideBridgeFactoryJni;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.MockTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.state.PriceDropMetricsLogger.MetricsResult;
import org.chromium.chrome.test.ChromeBrowserTestRule;
import org.chromium.components.optimization_guide.OptimizationGuideDecision;
import org.chromium.components.optimization_guide.proto.HintsProto;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;

import java.nio.ByteBuffer;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/** Test relating to {@link ShoppingPersistedTabData} */
@RunWith(BaseJUnit4ClassRunner.class)
@EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    "force-fieldtrials=Study/Group"
})
public class ShoppingPersistedTabDataTest {
    @Rule public final ChromeBrowserTestRule mBrowserTestRule = new ChromeBrowserTestRule();

    @Rule public JniMocker mMocker = new JniMocker();

    @Mock protected OptimizationGuideBridgeFactory.Natives mOptimizationGuideBridgeFactoryJniMock;
    @Mock protected OptimizationGuideBridge mOptimizationGuideBridgeMock;

    @Mock protected Profile mProfileMock;

    @Mock protected NavigationHandle mNavigationHandle;

    // For R8 optimizer - see b/303266326.
    @Mock private Tab mDoNotUseTab;

    @Mock private ShoppingPersistedTabDataService mShoppingPersistedTabDataService;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mMocker.mock(
                OptimizationGuideBridgeFactoryJni.TEST_HOOKS,
                mOptimizationGuideBridgeFactoryJniMock);
        doReturn(mOptimizationGuideBridgeMock)
                .when(mOptimizationGuideBridgeFactoryJniMock)
                .getForProfile(mProfileMock);

        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.onDeferredStartup();
                    PersistedTabDataConfiguration.setUseTestConfig(true);
                });
        doReturn(ShoppingPersistedTabDataTestUtils.IS_INCOGNITO)
                .when(mProfileMock)
                .isOffTheRecord();
        ProfileManager.setLastUsedProfileForTesting(mProfileMock);
        PriceTrackingFeatures.setPriceTrackingEnabledForTesting(false);
        doReturn(true).when(mNavigationHandle).isInPrimaryMainFrame();
        ShoppingPersistedTabDataService.setServiceForTesting(mShoppingPersistedTabDataService);
    }

    @UiThreadTest
    @SmallTest
    @Test
    @CommandLineFlags.Add({"force-fieldtrial-params=Study.Group:check_if_price_drop_is_seen/true"})
    public void testShoppingProto() {
        Tab tab = new MockTab(ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        ObservableSupplierImpl<Boolean> supplier = new ObservableSupplierImpl<>();
        supplier.set(true);
        shoppingPersistedTabData.registerIsTabSaveEnabledSupplier(supplier);
        shoppingPersistedTabData.setPriceMicros(ShoppingPersistedTabDataTestUtils.PRICE_MICROS);
        shoppingPersistedTabData.setCurrencyCode(
                ShoppingPersistedTabDataTestUtils.GREAT_BRITAIN_CURRENCY_CODE);
        shoppingPersistedTabData.setPriceDropGurl(new GURL("https://www.google.com"));
        shoppingPersistedTabData.setIsCurrentPriceDropSeen(true);
        shoppingPersistedTabData.setProductTitle(
                ShoppingPersistedTabDataTestUtils.FAKE_PRODUCT_TITLE);
        shoppingPersistedTabData.setProductImageUrl(
                new GURL(ShoppingPersistedTabDataTestUtils.FAKE_PRODUCT_IMAGE_URL));
        ByteBuffer serialized = shoppingPersistedTabData.getSerializer().get();
        ShoppingPersistedTabData deserialized = new ShoppingPersistedTabData(tab);
        deserialized.deserialize(serialized);
        Assert.assertEquals(
                ShoppingPersistedTabDataTestUtils.PRICE_MICROS, deserialized.getPriceMicros());
        Assert.assertEquals(
                ShoppingPersistedTabData.NO_PRICE_KNOWN, deserialized.getPreviousPriceMicros());
        Assert.assertEquals(
                ShoppingPersistedTabDataTestUtils.GREAT_BRITAIN_CURRENCY_CODE,
                deserialized.getCurrencyCode());
        Assert.assertTrue(deserialized.getIsCurrentPriceDropSeen());
        Assert.assertEquals(
                new GURL("https://www.google.com"), deserialized.getPriceDropDataForTesting().gurl);
        Assert.assertEquals(
                ShoppingPersistedTabDataTestUtils.FAKE_PRODUCT_TITLE,
                deserialized.getProductTitle());
        Assert.assertEquals(
                new GURL(ShoppingPersistedTabDataTestUtils.FAKE_PRODUCT_IMAGE_URL),
                deserialized.getProductImageUrl());
        MetricsResult metricsResult =
                deserialized.getPriceDropMetricsLoggerForTesting().getMetricsResultForTesting();
        Assert.assertFalse(metricsResult.isProductDetailPage);
        Assert.assertTrue(metricsResult.containsPrice);
        Assert.assertFalse(metricsResult.containsPriceDrop);
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testMetricDerivations() {
        Tab tab = new MockTab(ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        ObservableSupplierImpl<Boolean> supplier = new ObservableSupplierImpl<>();
        supplier.set(true);
        shoppingPersistedTabData.registerIsTabSaveEnabledSupplier(supplier);
        for (boolean isProductDetailPage : new boolean[] {false, true}) {
            for (boolean containsPrice : new boolean[] {false, true}) {
                for (boolean containsPriceDrop : new boolean[] {false, true}) {
                    shoppingPersistedTabData.setMainOfferId(
                            isProductDetailPage ? "non-empty-offer-id" : null);
                    shoppingPersistedTabData.setPriceMicros(
                            containsPrice || containsPriceDrop
                                    ? 42_000_000L
                                    : ShoppingPersistedTabData.NO_PRICE_KNOWN);
                    shoppingPersistedTabData.setPreviousPriceMicros(
                            containsPriceDrop
                                    ? 30_000_000L
                                    : ShoppingPersistedTabData.NO_PRICE_KNOWN);
                    ByteBuffer serialized = shoppingPersistedTabData.getSerializer().get();
                    ShoppingPersistedTabData deserialized = new ShoppingPersistedTabData(tab);
                    deserialized.deserialize(serialized);
                    MetricsResult metricsResult =
                            deserialized
                                    .getPriceDropMetricsLoggerForTesting()
                                    .getMetricsResultForTesting();
                    Assert.assertEquals(isProductDetailPage, metricsResult.isProductDetailPage);
                    Assert.assertEquals(
                            containsPrice || containsPriceDrop, metricsResult.containsPrice);
                    Assert.assertEquals(containsPriceDrop, metricsResult.containsPriceDrop);
                }
            }
        }
    }

    @SmallTest
    @Test
    public void testStaleTab() {
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        Semaphore semaphore = new Semaphore(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.setTimestampMillis(
                            System.currentTimeMillis() - TimeUnit.DAYS.toMillis(100));
                    ShoppingPersistedTabData.from(
                            tab,
                            (shoppingPersistedTabData) -> {
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
        ShoppingPersistedTabDataTestUtils.verifyPriceTrackingOptimizationTypeCalled(
                mOptimizationGuideBridgeMock, 0);
    }

    @SmallTest
    @Test
    @CommandLineFlags.Add({
        "force-fieldtrial-params=Study.Group:price_tracking_stale_tab_threshold_seconds/86400"
    })
    public void test2DayTabWithStaleOverride1day() {
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        Semaphore semaphore = new Semaphore(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.setTimestampMillis(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2));
                    ShoppingPersistedTabData.from(
                            tab,
                            (shoppingPersistedTabData) -> {
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
        ShoppingPersistedTabDataTestUtils.verifyPriceTrackingOptimizationTypeCalled(
                mOptimizationGuideBridgeMock, 0);
    }

    @SmallTest
    @Test
    @CommandLineFlags.Add({
        "force-fieldtrial-params=Study.Group:price_tracking_stale_tab_threshold_seconds/86400"
    })
    public void testHalfDayTabWithStaleOverride1day() {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_INITIAL);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        Semaphore semaphore = new Semaphore(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.setTimestampMillis(
                            System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12));
                    ShoppingPersistedTabData.from(
                            tab,
                            (shoppingPersistedTabData) -> {
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
        ShoppingPersistedTabDataTestUtils.verifyPriceTrackingOptimizationTypeCalled(
                mOptimizationGuideBridgeMock, 1);
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testPriceDrop() {
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils.createShoppingPersistedTabDataWithDefaults(
                        mProfileMock);
        // Prices unknown is not a price drop
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());

        // Same price is not a price drop
        shoppingPersistedTabData.setPriceMicros(
                ShoppingPersistedTabDataTestUtils.HIGH_PRICE_MICROS);
        shoppingPersistedTabData.setPreviousPriceMicros(
                ShoppingPersistedTabDataTestUtils.HIGH_PRICE_MICROS);
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());

        // Lower -> Higher price is not a price drop
        shoppingPersistedTabData.setPriceMicros(
                ShoppingPersistedTabDataTestUtils.HIGH_PRICE_MICROS);
        shoppingPersistedTabData.setPreviousPriceMicros(
                ShoppingPersistedTabDataTestUtils.LOW_PRICE_MICROS);
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());

        // Actual price drop (Higher -> Lower)
        shoppingPersistedTabData.setPriceMicros(ShoppingPersistedTabDataTestUtils.LOW_PRICE_MICROS);
        shoppingPersistedTabData.setPreviousPriceMicros(
                ShoppingPersistedTabDataTestUtils.HIGH_PRICE_MICROS);
        ShoppingPersistedTabData.PriceDrop priceDrop = shoppingPersistedTabData.getPriceDrop();
        Assert.assertEquals(ShoppingPersistedTabDataTestUtils.LOW_PRICE_FORMATTED, priceDrop.price);
        Assert.assertEquals(
                ShoppingPersistedTabDataTestUtils.HIGH_PRICE_FORMATTED, priceDrop.previousPrice);
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testPriceDropFilterSamePrice() {
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils.createShoppingPersistedTabDataWithDefaults(
                        mProfileMock);

        // $10 -> $10 is not a price drop (same price)
        shoppingPersistedTabData.setPreviousPriceMicrosForTesting(10_000_000L);
        shoppingPersistedTabData.setPriceMicrosForTesting(10_000_000L);
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testPriceDropFilterNoFormattedPriceDifference() {
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils.createShoppingPersistedTabDataWithDefaults(
                        mProfileMock);

        // $10.40 -> $10 (which would be displayed $10 -> $10 is not a price drop)
        shoppingPersistedTabData.setPreviousPriceMicrosForTesting(10_400_000L);
        shoppingPersistedTabData.setPriceMicrosForTesting(10_000_000L);
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testPriceDropFilterPriceIncrease() {
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils.createShoppingPersistedTabDataWithDefaults(
                        mProfileMock);

        // $9.33 -> $9.66 price increase is not a price drop
        shoppingPersistedTabData.setPreviousPriceMicrosForTesting(9_330_000L);
        shoppingPersistedTabData.setPriceMicrosForTesting(9_660_000L);
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testPriceDropGBP() {
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils.createShoppingPersistedTabDataWithCurrencyCode(
                        ShoppingPersistedTabDataTestUtils.TAB_ID,
                        mProfileMock,
                        ShoppingPersistedTabDataTestUtils.GREAT_BRITAIN_CURRENCY_CODE);
        shoppingPersistedTabData.setPreviousPriceMicrosForTesting(15_000_000L);
        shoppingPersistedTabData.setPriceMicrosForTesting(9_560_000L);
        Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
        Assert.assertEquals("£15", shoppingPersistedTabData.getPriceDrop().previousPrice);
        Assert.assertEquals("£9.56", shoppingPersistedTabData.getPriceDrop().price);
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testPriceDropJPY() {
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils.createShoppingPersistedTabDataWithCurrencyCode(
                        ShoppingPersistedTabDataTestUtils.TAB_ID,
                        mProfileMock,
                        ShoppingPersistedTabDataTestUtils.JAPAN_CURRENCY_CODE);
        shoppingPersistedTabData.setPreviousPriceMicrosForTesting(3_140_000_000_000L);
        shoppingPersistedTabData.setPriceMicrosForTesting(287_000_000_000L);
        Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
        Assert.assertEquals("¥3,140,000", shoppingPersistedTabData.getPriceDrop().previousPrice);
        Assert.assertEquals("¥287,000", shoppingPersistedTabData.getPriceDrop().price);
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testStalePriceDropUSD() {
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils.createShoppingPersistedTabDataWithCurrencyCode(
                        ShoppingPersistedTabDataTestUtils.TAB_ID,
                        mProfileMock,
                        ShoppingPersistedTabDataTestUtils.UNITED_STATES_CURRENCY_CODE);
        // $10 -> $5 (50% and $5 price drop is big enough)
        shoppingPersistedTabData.setPreviousPriceMicrosForTesting(10000000L);
        shoppingPersistedTabData.setPriceMicrosForTesting(5000000L);
        Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
        Assert.assertEquals("$5.00", shoppingPersistedTabData.getPriceDrop().price);
        Assert.assertEquals("$10", shoppingPersistedTabData.getPriceDrop().previousPrice);

        // Price drops greater than a week old are removed
        shoppingPersistedTabData.setLastPriceChangeTimeMsForTesting(
                System.currentTimeMillis() - TimeUnit.DAYS.toMillis(8));
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testNewUrlResetSPTD() {
        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.NONE);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
        NavigationHandle navigationHandle = mock(NavigationHandle.class);
        for (boolean isSameDocument : new boolean[] {false, true}) {
            ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
            shoppingPersistedTabData.setPriceMicros(42_000_000L);
            shoppingPersistedTabData.setPreviousPriceMicros(60_000_000L);
            shoppingPersistedTabData.setCurrencyCode("USD");
            shoppingPersistedTabData.setPriceDropGurl(
                    ShoppingPersistedTabDataTestUtils.DEFAULT_GURL);
            Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
            doReturn(isSameDocument).when(navigationHandle).isSameDocument();
            shoppingPersistedTabData
                    .getUrlUpdatedObserverForTesting()
                    .onDidStartNavigationInPrimaryMainFrame(tab, navigationHandle);
            if (!isSameDocument) {
                Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
            } else {
                Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
            }
        }
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testDontResetSPTDOnRefresh() {
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.NONE);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
        NavigationHandle navigationHandle = mock(NavigationHandle.class);
        doReturn(true).when(navigationHandle).isInPrimaryMainFrame();
        doReturn(false).when(navigationHandle).isSameDocument();
        GURL gurl1 = new GURL("https://foo.com");
        GURL gurl2 = new GURL("https://bar.com");
        tab.setGurlOverrideForTesting(gurl1);
        doReturn(gurl1).when(navigationHandle).getUrl();
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        shoppingPersistedTabData.setPriceMicros(42_000_000L);
        shoppingPersistedTabData.setPreviousPriceMicros(60_000_000L);
        shoppingPersistedTabData.setCurrencyCode("USD");
        shoppingPersistedTabData.setPriceDropGurl(gurl1);
        Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
        shoppingPersistedTabData
                .getUrlUpdatedObserverForTesting()
                .onDidStartNavigationInPrimaryMainFrame(tab, navigationHandle);
        Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
        doReturn(gurl2).when(navigationHandle).getUrl();
        shoppingPersistedTabData
                .getUrlUpdatedObserverForTesting()
                .onDidStartNavigationInPrimaryMainFrame(tab, navigationHandle);
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testOmniBoxSearchResetSPTD() {
        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.NONE);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
        NavigationHandle navigationHandle = mock(NavigationHandle.class);
        doReturn(false).when(navigationHandle).isSameDocument();
        doReturn(false).when(navigationHandle).isValidSearchFormUrl();
        doReturn(true).when(navigationHandle).hasCommitted();
        int reloadFromAddressBar = PageTransition.FROM_ADDRESS_BAR | PageTransition.RELOAD;
        doReturn(reloadFromAddressBar).when(navigationHandle).pageTransition();
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        shoppingPersistedTabData.setPriceMicros(42_000_000L);
        shoppingPersistedTabData.setPreviousPriceMicros(60_000_000L);
        shoppingPersistedTabData.setCurrencyCode("USD");
        shoppingPersistedTabData.setPriceDropGurl(ShoppingPersistedTabDataTestUtils.DEFAULT_GURL);
        Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
        shoppingPersistedTabData
                .getUrlUpdatedObserverForTesting()
                .onDidFinishNavigationInPrimaryMainFrame(tab, navigationHandle);
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
    }

    @SmallTest
    @Test
    public void testSPTDSavingEnabledUponSuccessfulProductUpdateResponse() {
        final Semaphore semaphore = new Semaphore(0);
        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_AND_PRODUCT_UPDATE);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            tab,
                            (shoppingPersistedTabData) -> {
                                Assert.assertTrue(
                                        shoppingPersistedTabData.mIsTabSaveEnabledSupplier.get());
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
    }

    @SmallTest
    @Test
    public void testSPTDNullUponUnsuccessfulResponse() {
        final Semaphore semaphore = new Semaphore(0);
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.BUYABLE_PRODUCT_EMPTY);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            tab,
                            (shoppingPersistedTabData) -> {
                                Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
    }

    @SmallTest
    @Test
    public void testSPTDNullOptimizationGuideFalse() {
        final Semaphore semaphore = new Semaphore(0);
        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.NONE);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            tab,
                            (shoppingPersistedTabData) -> {
                                Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testSerializationBug() {
        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        shoppingPersistedTabData.setPriceMicros(42_000_000L);
        ByteBuffer serialized = shoppingPersistedTabData.getSerializer().get();
        PersistedTabDataConfiguration config =
                PersistedTabDataConfiguration.get(
                        ShoppingPersistedTabData.class, tab.isIncognito());
        ShoppingPersistedTabData deserialized =
                new ShoppingPersistedTabData(tab, config.getStorage(), config.getId());
        deserialized.deserializeAndLog(serialized);
        Assert.assertEquals(42_000_000L, deserialized.getPriceMicros());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testSerializeWithOfferId() {
        Tab tab = new MockTab(ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        ObservableSupplierImpl<Boolean> supplier = new ObservableSupplierImpl<>();
        supplier.set(true);
        shoppingPersistedTabData.registerIsTabSaveEnabledSupplier(supplier);
        shoppingPersistedTabData.setMainOfferId(ShoppingPersistedTabDataTestUtils.FAKE_OFFER_ID);
        ByteBuffer serialized = shoppingPersistedTabData.getSerializer().get();
        ShoppingPersistedTabData deserialized = new ShoppingPersistedTabData(tab);
        deserialized.deserialize(serialized);
        Assert.assertEquals(
                ShoppingPersistedTabDataTestUtils.FAKE_OFFER_ID, deserialized.getMainOfferId());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testOptimizationGuideNavigationIntegration() {
        Tab tab = new MockTab(ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        GURL gurl = new GURL("https://www.google.com");
        doReturn(gurl).when(mNavigationHandle).getUrl();
        doReturn(true).when(mNavigationHandle).hasCommitted();
        shoppingPersistedTabData
                .getUrlUpdatedObserverForTesting()
                .onDidFinishNavigationInPrimaryMainFrame(tab, mNavigationHandle);
        ShoppingPersistedTabDataTestUtils.verifyOptimizationGuideCalledWithNavigationHandle(
                mOptimizationGuideBridgeMock, gurl);
    }

    @UiThreadTest
    @SmallTest
    @Test
    @EnableFeatures({ChromeFeatureList.PRICE_CHANGE_MODULE})
    public void testOptGuidePrefetching() {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponseAsync(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_AND_PRODUCT_UPDATE);
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        tab.setIsInitialized(true);
        GURL gurl = new GURL("https://www.google.com");
        tab.setGurlOverrideForTesting(gurl);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        doReturn(gurl).when(mNavigationHandle).getUrl();
        Semaphore semaphore = new Semaphore(0);
        shoppingPersistedTabData.prefetchOnNewNavigation(
                tab, mNavigationHandle, semaphore::release);
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
        Assert.assertEquals(287_000_000L, shoppingPersistedTabData.getPriceMicros());
        Assert.assertEquals(
                123_456_789_012_345L, shoppingPersistedTabData.getPreviousPriceMicros());
        Assert.assertEquals(
                ShoppingPersistedTabDataTestUtils.FAKE_PRODUCT_TITLE,
                shoppingPersistedTabData.getProductTitle());
        Assert.assertEquals(
                ShoppingPersistedTabDataTestUtils.FAKE_PRODUCT_IMAGE_URL,
                shoppingPersistedTabData.getProductImageUrl().getSpec());
        verify(mShoppingPersistedTabDataService, times(1)).notifyPriceDropStatus(tab, true);
    }

    @UiThreadTest
    @SmallTest
    @Test
    @EnableFeatures({ChromeFeatureList.PRICE_CHANGE_MODULE})
    public void testOptGuidePrefetchingNoResponse() {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponseAsync(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.NONE);
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        tab.setIsInitialized(true);
        GURL gurl = new GURL("https://www.google.com");
        tab.setGurlOverrideForTesting(gurl);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        doReturn(gurl).when(mNavigationHandle).getUrl();
        Semaphore semaphore = new Semaphore(0);
        shoppingPersistedTabData.prefetchOnNewNavigation(
                tab, mNavigationHandle, semaphore::release);
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
        Assert.assertEquals(
                ShoppingPersistedTabData.NO_PRICE_KNOWN, shoppingPersistedTabData.getPriceMicros());
        Assert.assertEquals(
                ShoppingPersistedTabData.NO_PRICE_KNOWN,
                shoppingPersistedTabData.getPreviousPriceMicros());
        verify(mShoppingPersistedTabDataService, times(1)).notifyPriceDropStatus(tab, false);
    }

    @UiThreadTest
    @SmallTest
    @Test
    @EnableFeatures({ChromeFeatureList.PRICE_CHANGE_MODULE})
    public void testOptGuidePrefetchingUnparseable() {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponseAsync(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.UNPARSEABLE);
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        tab.setIsInitialized(true);
        GURL gurl = new GURL("https://www.google.com");
        tab.setGurlOverrideForTesting(gurl);
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        doReturn(gurl).when(mNavigationHandle).getUrl();
        Semaphore semaphore = new Semaphore(0);
        shoppingPersistedTabData.prefetchOnNewNavigation(
                tab, mNavigationHandle, semaphore::release);
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
        Assert.assertEquals(
                ShoppingPersistedTabData.NO_PRICE_KNOWN, shoppingPersistedTabData.getPriceMicros());
        Assert.assertEquals(
                ShoppingPersistedTabData.NO_PRICE_KNOWN,
                shoppingPersistedTabData.getPreviousPriceMicros());
        verify(mShoppingPersistedTabDataService, times(0))
                .notifyPriceDropStatus(any(), anyBoolean());
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testPriceDropURLTabURLMisMatch() {
        MockTab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        tab.setIsInitialized(true);
        tab.setGurlOverrideForTesting(new GURL("https://www.google.com"));
        ShoppingPersistedTabData shoppingPersistedTabData = new ShoppingPersistedTabData(tab);
        shoppingPersistedTabData.setPriceMicros(42_000_000L);
        shoppingPersistedTabData.setPreviousPriceMicros(60_000_000L);
        shoppingPersistedTabData.setPriceDropGurl(new GURL("https://www.google.com"));
        shoppingPersistedTabData.setLastPriceChangeTimeMsForTesting(System.currentTimeMillis());
        shoppingPersistedTabData.setCurrencyCode("USD");
        shoppingPersistedTabData.setLastUpdatedMs(System.currentTimeMillis());
        Assert.assertNotNull(shoppingPersistedTabData.getPriceDrop());
        Assert.assertFalse(shoppingPersistedTabData.needsUpdate());
        shoppingPersistedTabData.setPriceDropGurl(new GURL("https://www.yahoo.com"));
        Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
        Assert.assertTrue(shoppingPersistedTabData.needsUpdate());
    }

    @SmallTest
    @Test
    public void testIncognitoTabDisabled() throws TimeoutException {
        Tab tab = mock(Tab.class);
        doReturn(true).when(tab).isIncognito();
        CallbackHelper callbackHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            tab,
                            (res) -> {
                                Assert.assertNull(res);
                                callbackHelper.notifyCalled();
                            });
                });
        callbackHelper.waitForCallback(0);
    }

    @SmallTest
    @Test
    public void testCustomTabsDisabled() throws TimeoutException {
        Tab tab = mock(Tab.class);
        doReturn(true).when(tab).isCustomTab();
        CallbackHelper callbackHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            tab,
                            (res) -> {
                                Assert.assertNull(res);
                                callbackHelper.notifyCalled();
                            });
                });
        callbackHelper.waitForCallback(0);
    }

    @SmallTest
    @Test
    public void testTabDestroyedSupplier() {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_INITIAL);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
        MockTab tab =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            MockTab mockTab =
                                    MockTab.createAndInitialize(
                                            ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
                            long timestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
                            mockTab.setTimestampMillis(timestamp);
                            return mockTab;
                        });
        for (boolean isDestroyed : new boolean[] {false, true}) {
            Semaphore semaphore = new Semaphore(0);
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        if (isDestroyed) tab.destroy();
                        ShoppingPersistedTabData.from(
                                tab,
                                (shoppingPersistedTabData) -> {
                                    if (isDestroyed) {
                                        Assert.assertNull(shoppingPersistedTabData);
                                    } else {
                                        Assert.assertNotNull(shoppingPersistedTabData);
                                    }
                                    semaphore.release();
                                });
                    });
            ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
        }
    }

    @SmallTest
    @Test
    public void testTabDestroyed1() {
        final Semaphore semaphore = new Semaphore(0);
        MockTab tab = getDefaultTab();
        mockOptimizationGuideDefaults();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // There is ShoppingPersistedTabData associated with the Tab, however, it is 1
                    // day old (the threshold for a refetch is 1 hour) so a refetch will be
                    // forced.
                    ShoppingPersistedTabData shoppingPersistedTabData =
                            new ShoppingPersistedTabData(tab);
                    shoppingPersistedTabData.setLastUpdatedMs(
                            System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
                    tab.getUserDataHost()
                            .setUserData(ShoppingPersistedTabData.class, shoppingPersistedTabData);
                    // Tab being destroyed should result in the public API from returning null
                    tab.destroy();
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptdRes) -> {
                                Assert.assertNull(sptdRes);
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
    }

    @SmallTest
    @Test
    public void testTabDestroyed2() {
        final Semaphore semaphore = new Semaphore(0);
        MockTab tab = getDefaultTab();
        mockOptimizationGuideDefaults();
        // There is no ShoppingPersistedTabData associated with the Tab, so it will be
        // acquired from OptimizationGuide.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Tab being destroyed should result in the public API from returning null
                    tab.destroy();
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptdRes) -> {
                                Assert.assertNull(sptdRes);
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
    }

    @SmallTest
    @Test
    public void testTabDestroyed3() {
        final Semaphore semaphore0 = new Semaphore(0);
        MockTab tab = getDefaultTab();
        mockOptimizationGuideDefaults();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData shoppingPersistedTabData =
                            new ShoppingPersistedTabData(tab);
                    // ShoppingPersistedTabData is 1 day old which will trigger a refetch, however,
                    // this time it will be acquired from storage, then refetched.
                    shoppingPersistedTabData.setLastUpdatedMs(
                            System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
                    save(shoppingPersistedTabData);
                    // Verify ShoppingPersistedTabData is acquired from storage as expected.
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptdRes) -> {
                                Assert.assertNotNull(sptdRes);
                                semaphore0.release();
                            });
                });
        final Semaphore semaphore1 = new Semaphore(0);
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Remove UserData to force acquisition from storage.
                    tab.getUserDataHost().removeUserData(ShoppingPersistedTabData.class);
                    // Tab being destroyed should result in the public API from returning null
                    tab.destroy();
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptdRes) -> {
                                Assert.assertNull(sptdRes);
                                semaphore1.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore1);
    }

    @SmallTest
    @Test
    public void testTabDestroyed4() {
        final Semaphore semaphore0 = new Semaphore(0);
        MockTab tab = getDefaultTab();
        tab.setGurlOverrideForTesting(ShoppingPersistedTabDataTestUtils.DEFAULT_GURL);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData shoppingPersistedTabData =
                            new ShoppingPersistedTabData(tab);
                    // ShoppingPersistedTabData is 0 seconds old so it can be acquired from storage
                    // and returned direcetly, without a refresh.
                    shoppingPersistedTabData.setLastUpdatedMs(System.currentTimeMillis());
                    shoppingPersistedTabData.setPriceDropGurl(
                            ShoppingPersistedTabDataTestUtils.DEFAULT_GURL);
                    save(shoppingPersistedTabData);
                    // Verify ShoppingPersistedTabData is acquired from storage as expected.
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptdRes) -> {
                                Assert.assertNotNull(sptdRes);
                                semaphore0.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore0);
        final Semaphore semaphore1 = new Semaphore(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Remove UserData to force acquisition from storage.
                    tab.getUserDataHost().removeUserData(ShoppingPersistedTabData.class);
                    // Tab being destroyed should result in the public API from returning null
                    tab.destroy();
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptdRes) -> {
                                Assert.assertNull(sptdRes);
                                semaphore1.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore1);
    }

    @SmallTest
    @Test
    public void testCheckPriceDropUrlForUpdateWhenItExists() {
        final Semaphore semaphore = new Semaphore(0);
        MockTab tab = getDefaultTab();
        mockOptimizationGuideEmptyResponse();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptdRes) -> {
                                Assert.assertNull(sptdRes.getPriceDropDataForTesting().gurl);
                                Assert.assertFalse(sptdRes.needsUpdate());
                                semaphore.release();
                            });
                });
        ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
    }

    private MockTab getDefaultTab() {
        return ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
    }

    private void mockOptimizationGuideDefaults() {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_AND_PRODUCT_UPDATE);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
    }

    private void mockOptimizationGuideEmptyResponse() {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse.BUYABLE_PRODUCT_EMPTY);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);
    }

    private static void save(ShoppingPersistedTabData shoppingPersistedTabData) {
        ObservableSupplierImpl<Boolean> supplier = new ObservableSupplierImpl<>();
        supplier.set(true);
        shoppingPersistedTabData.registerIsTabSaveEnabledSupplier(supplier);
        shoppingPersistedTabData.enableSaving();
        shoppingPersistedTabData.save();
    }

    @UiThreadTest
    @SmallTest
    @Test
    public void testShoppingPersistedTabDataSupportedForMaintenance() {
        Assert.assertTrue(
                PersistedTabData.getSupportedMaintenanceClassesForTesting()
                        .contains(ShoppingPersistedTabData.class));
    }

    @SmallTest
    @Test
    public void testVerifyDeserializationBackgroundThread() throws TimeoutException {
        CallbackHelper helper = new CallbackHelper();
        int count = helper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Tab tab = MockTab.createAndInitialize(1, mProfileMock);
                    DeserializeAndLogCheckerShoppingPersistedTabData deserializeChecker =
                            new DeserializeAndLogCheckerShoppingPersistedTabData(tab);
                    registerObserverSupplier(deserializeChecker);
                    deserializeChecker.save();
                    PersistedTabData.from(
                            tab,
                            (data, storage, id, factoryCallback) -> {
                                factoryCallback.onResult(
                                        new DeserializeAndLogCheckerShoppingPersistedTabData(
                                                tab, storage, id));
                            },
                            null,
                            ShoppingPersistedTabData.class,
                            (res) -> {
                                helper.notifyCalled();
                            });
                });
        helper.waitForCallback(count);
    }

    @SmallTest
    @Test
    public void testDestroyedTab() throws TimeoutException {
        Tab tab = mock(Tab.class);
        doReturn(true).when(tab).isDestroyed();
        CallbackHelper helper = new CallbackHelper();
        int count = helper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            tab,
                            (res) -> {
                                Assert.assertNull(res);
                                helper.notifyCalled();
                            });
                });
        helper.waitForCallback(count);
    }

    @SmallTest
    @Test
    public void testNullTab() throws TimeoutException {
        CallbackHelper helper = new CallbackHelper();
        int count = helper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ShoppingPersistedTabData.from(
                            null,
                            (res) -> {
                                Assert.assertNull(res);
                                helper.notifyCalled();
                            });
                });
        helper.waitForCallback(count);
    }

    static class DeserializeAndLogCheckerShoppingPersistedTabData extends ShoppingPersistedTabData {
        DeserializeAndLogCheckerShoppingPersistedTabData(Tab tab) {
            super(tab);
        }

        DeserializeAndLogCheckerShoppingPersistedTabData(
                Tab tab,
                PersistedTabDataStorage persistedTabDataStorage,
                String persistedTabDataId) {
            super(tab, persistedTabDataStorage, persistedTabDataId);
        }

        @Override
        public void deserializeAndLog(@Nullable ByteBuffer bytes) {
            ThreadUtils.assertOnBackgroundThread();
            super.deserializeAndLog(bytes);
        }

        @Override
        protected boolean needsUpdate() {
            return false;
        }
    }

    @SmallTest
    @Test
    @CommandLineFlags.Add({"force-fieldtrial-params=Study.Group:check_if_price_drop_is_seen/true"})
    public void testIsCurrentPriceDropSeen_PriceChange() throws TimeoutException {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_AND_PRODUCT_UPDATE);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);

        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils
                        .createShoppingPersistedTabDataWithPriceDropOnUiThread(tab);

        CallbackHelper callbackHelper = new CallbackHelper();
        int count = callbackHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    shoppingPersistedTabData.setIsCurrentPriceDropSeen(true);
                    shoppingPersistedTabData.setPriceMicros(
                            ShoppingPersistedTabDataTestUtils.LOW_PRICE_MICROS);
                    tab.getUserDataHost()
                            .setUserData(ShoppingPersistedTabData.class, shoppingPersistedTabData);
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptd) -> {
                                Assert.assertFalse(sptd.getIsCurrentPriceDropSeen());
                                callbackHelper.notifyCalled();
                            });
                });
        callbackHelper.waitForCallback(count);
    }

    @SmallTest
    @Test
    @CommandLineFlags.Add({"force-fieldtrial-params=Study.Group:check_if_price_drop_is_seen/true"})
    public void testIsCurrentPriceDropSeen_CurrencyChange() throws TimeoutException {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_AND_PRODUCT_UPDATE);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);

        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils
                        .createShoppingPersistedTabDataWithPriceDropOnUiThread(tab);

        CallbackHelper callbackHelper = new CallbackHelper();
        int count = callbackHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    shoppingPersistedTabData.setIsCurrentPriceDropSeen(true);
                    shoppingPersistedTabData.setCurrencyCode(
                            ShoppingPersistedTabDataTestUtils.JAPAN_CURRENCY_CODE);
                    tab.getUserDataHost()
                            .setUserData(ShoppingPersistedTabData.class, shoppingPersistedTabData);
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptd) -> {
                                Assert.assertFalse(sptd.getIsCurrentPriceDropSeen());
                                callbackHelper.notifyCalled();
                            });
                });
        callbackHelper.waitForCallback(count);
    }

    @SmallTest
    @Test
    @CommandLineFlags.Add({"force-fieldtrial-params=Study.Group:check_if_price_drop_is_seen/true"})
    public void testIsCurrentPriceDropSeen_NoPriceChange() throws TimeoutException {
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.PRICE_TRACKING,
                ShoppingPersistedTabDataTestUtils.MockPriceTrackingResponse
                        .BUYABLE_PRODUCT_AND_PRODUCT_UPDATE);
        ShoppingPersistedTabDataTestUtils.mockOptimizationGuideResponse(
                mOptimizationGuideBridgeMock,
                HintsProto.OptimizationType.SHOPPING_PAGE_PREDICTOR,
                OptimizationGuideDecision.TRUE,
                null);

        Tab tab =
                ShoppingPersistedTabDataTestUtils.createTabOnUiThread(
                        ShoppingPersistedTabDataTestUtils.TAB_ID, mProfileMock);
        ShoppingPersistedTabData shoppingPersistedTabData =
                ShoppingPersistedTabDataTestUtils
                        .createShoppingPersistedTabDataWithPriceDropOnUiThread(tab);

        CallbackHelper callbackHelper = new CallbackHelper();
        int count = callbackHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    shoppingPersistedTabData.setIsCurrentPriceDropSeen(true);
                    tab.getUserDataHost()
                            .setUserData(ShoppingPersistedTabData.class, shoppingPersistedTabData);
                    ShoppingPersistedTabData.from(
                            tab,
                            (sptd) -> {
                                Assert.assertTrue(sptd.getIsCurrentPriceDropSeen());
                                callbackHelper.notifyCalled();
                            });
                });
        callbackHelper.waitForCallback(count);
    }

    private static void registerObserverSupplier(
            ShoppingPersistedTabData shoppingPersistedTabData) {
        ObservableSupplierImpl<Boolean> supplier = new ObservableSupplierImpl<>();
        supplier.set(true);
        shoppingPersistedTabData.registerIsTabSaveEnabledSupplier(supplier);
    }
}