/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.timedetector;

import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_EXTERNAL;
import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_GNSS;
import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_NETWORK;
import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_TELEPHONY;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.app.time.ExternalTimeSuggestion;
import android.app.timedetector.GnssTimeSuggestion;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.TelephonyTimeSuggestion;
import android.os.TimestampedValue;

import androidx.test.runner.AndroidJUnit4;

import com.android.server.timedetector.TimeDetectorStrategy.Origin;
import com.android.server.timezonedetector.ConfigurationChangeListener;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Objects;

@RunWith(AndroidJUnit4.class)
public class TimeDetectorStrategyImplTest {

    private static final Instant TIME_LOWER_BOUND = createUnixEpochTime(2009, 1, 1, 12, 0, 0);

    private static final TimestampedValue<Instant> ARBITRARY_CLOCK_INITIALIZATION_INFO =
            new TimestampedValue<>(
                    123456789L /* realtimeClockMillis */,
                    createUnixEpochTime(2010, 5, 23, 12, 0, 0));

    // This is the traditional ordering for time detection on Android.
    private static final @Origin int [] PROVIDERS_PRIORITY = { ORIGIN_TELEPHONY, ORIGIN_NETWORK };

    /**
     * An arbitrary time, very different from the {@link #ARBITRARY_CLOCK_INITIALIZATION_INFO}
     * time. Can be used as the basis for time suggestions.
     */
    private static final Instant ARBITRARY_TEST_TIME = createUnixEpochTime(2018, 1, 1, 12, 0, 0);

    private static final int ARBITRARY_SLOT_INDEX = 123456;

    private Script mScript;

    @Before
    public void setUp() {
        mScript = new Script();
    }

    @Test
    public void testSuggestTelephonyTime_autoTimeEnabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true);

        int slotIndex = ARBITRARY_SLOT_INDEX;
        Instant testTime = ARBITRARY_TEST_TIME;

        TelephonyTimeSuggestion timeSuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, testTime);
        mScript.simulateTimePassing()
                .simulateTelephonyTimeSuggestion(timeSuggestion);

        long expectedSystemClockMillis =
                mScript.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
        mScript.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis)
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion);
    }

    @Test
    public void testSuggestTelephonyTime_emptySuggestionIgnored() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true);

        int slotIndex = ARBITRARY_SLOT_INDEX;
        TelephonyTimeSuggestion timeSuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, null);
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, null);
    }

    @Test
    public void testSuggestTelephonyTime_systemClockThreshold() {
        final int systemClockUpdateThresholdMillis = 1000;
        final int clockIncrementMillis = 100;
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeThresholds(systemClockUpdateThresholdMillis)
                .pokeAutoTimeDetectionEnabled(true);

        int slotIndex = ARBITRARY_SLOT_INDEX;

        // Send the first time signal. It should be used.
        {
            TelephonyTimeSuggestion timeSuggestion1 =
                    mScript.generateTelephonyTimeSuggestion(slotIndex, ARBITRARY_TEST_TIME);

            // Increment the device clocks to simulate the passage of time.
            mScript.simulateTimePassing(clockIncrementMillis);

            long expectedSystemClockMillis1 =
                    mScript.calculateTimeInMillisForNow(timeSuggestion1.getUnixEpochTime());

            mScript.simulateTelephonyTimeSuggestion(timeSuggestion1)
                    .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1)
                    .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);
        }

        // Now send another time signal, but one that is too similar to the last one and should be
        // stored, but not used to set the system clock.
        {
            int underThresholdMillis = systemClockUpdateThresholdMillis - 1;
            TelephonyTimeSuggestion timeSuggestion2 = mScript.generateTelephonyTimeSuggestion(
                    slotIndex, mScript.peekSystemClockMillis() + underThresholdMillis);
            mScript.simulateTimePassing(clockIncrementMillis)
                    .simulateTelephonyTimeSuggestion(timeSuggestion2)
                    .verifySystemClockWasNotSetAndResetCallTracking()
                    .assertLatestTelephonySuggestion(slotIndex, timeSuggestion2);
        }

        // Now send another time signal, but one that is on the threshold and so should be used.
        {
            TelephonyTimeSuggestion timeSuggestion3 = mScript.generateTelephonyTimeSuggestion(
                    slotIndex,
                    mScript.peekSystemClockMillis() + systemClockUpdateThresholdMillis);
            mScript.simulateTimePassing(clockIncrementMillis);

            long expectedSystemClockMillis3 =
                    mScript.calculateTimeInMillisForNow(timeSuggestion3.getUnixEpochTime());

            mScript.simulateTelephonyTimeSuggestion(timeSuggestion3)
                    .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis3)
                    .assertLatestTelephonySuggestion(slotIndex, timeSuggestion3);
        }
    }

    @Test
    public void testSuggestTelephonyTime_multipleSlotIndexsAndBucketing() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true);

        // There are 2 slotIndexes in this test. slotIndex1 and slotIndex2 have different opinions
        // about the current time. slotIndex1 < slotIndex2 (which is important because the strategy
        // uses the lowest slotIndex when multiple telephony suggestions are available.
        int slotIndex1 = ARBITRARY_SLOT_INDEX;
        int slotIndex2 = ARBITRARY_SLOT_INDEX + 1;
        Instant slotIndex1Time = ARBITRARY_TEST_TIME;
        Instant slotIndex2Time = ARBITRARY_TEST_TIME.plus(Duration.ofDays(1));

        // Make a suggestion with slotIndex2.
        {
            TelephonyTimeSuggestion slotIndex2TimeSuggestion =
                    mScript.generateTelephonyTimeSuggestion(slotIndex2, slotIndex2Time);
            mScript.simulateTimePassing();

            long expectedSystemClockMillis = mScript.calculateTimeInMillisForNow(
                    slotIndex2TimeSuggestion.getUnixEpochTime());

            mScript.simulateTelephonyTimeSuggestion(slotIndex2TimeSuggestion)
                    .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis)
                    .assertLatestTelephonySuggestion(slotIndex1, null)
                    .assertLatestTelephonySuggestion(slotIndex2, slotIndex2TimeSuggestion);
        }

        mScript.simulateTimePassing();

        // Now make a different suggestion with slotIndex1.
        {
            TelephonyTimeSuggestion slotIndex1TimeSuggestion =
                    mScript.generateTelephonyTimeSuggestion(slotIndex1, slotIndex1Time);
            mScript.simulateTimePassing();

            long expectedSystemClockMillis = mScript.calculateTimeInMillisForNow(
                    slotIndex1TimeSuggestion.getUnixEpochTime());

            mScript.simulateTelephonyTimeSuggestion(slotIndex1TimeSuggestion)
                    .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis)
                    .assertLatestTelephonySuggestion(slotIndex1, slotIndex1TimeSuggestion);

        }

        mScript.simulateTimePassing();

        // Make another suggestion with slotIndex2. It should be stored but not used because the
        // slotIndex1 suggestion will still "win".
        {
            TelephonyTimeSuggestion slotIndex2TimeSuggestion =
                    mScript.generateTelephonyTimeSuggestion(slotIndex2, slotIndex2Time);
            mScript.simulateTimePassing();

            mScript.simulateTelephonyTimeSuggestion(slotIndex2TimeSuggestion)
                    .verifySystemClockWasNotSetAndResetCallTracking()
                    .assertLatestTelephonySuggestion(slotIndex2, slotIndex2TimeSuggestion);
        }

        // Let enough time pass that slotIndex1's suggestion should now be too old.
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.TELEPHONY_BUCKET_SIZE_MILLIS);

        // Make another suggestion with slotIndex2. It should be used because the slotIndex1
        // is in an older "bucket".
        {
            TelephonyTimeSuggestion slotIndex2TimeSuggestion =
                    mScript.generateTelephonyTimeSuggestion(slotIndex2, slotIndex2Time);
            mScript.simulateTimePassing();

            long expectedSystemClockMillis = mScript.calculateTimeInMillisForNow(
                    slotIndex2TimeSuggestion.getUnixEpochTime());

            mScript.simulateTelephonyTimeSuggestion(slotIndex2TimeSuggestion)
                    .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis)
                    .assertLatestTelephonySuggestion(slotIndex2, slotIndex2TimeSuggestion);
        }
    }

    @Test
    public void testSuggestTelephonyTime_autoTimeDisabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(false);

        int slotIndex = ARBITRARY_SLOT_INDEX;
        TelephonyTimeSuggestion timeSuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, ARBITRARY_TEST_TIME);
        mScript.simulateTimePassing()
                .simulateTelephonyTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion);
    }

    @Test
    public void testSuggestTelephonyTime_invalidNitzReferenceTimesIgnored() {
        final int systemClockUpdateThreshold = 2000;
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeThresholds(systemClockUpdateThreshold)
                .pokeAutoTimeDetectionEnabled(true);

        Instant testTime = ARBITRARY_TEST_TIME;
        int slotIndex = ARBITRARY_SLOT_INDEX;

        TelephonyTimeSuggestion timeSuggestion1 =
                mScript.generateTelephonyTimeSuggestion(slotIndex, testTime);
        TimestampedValue<Long> unixEpochTime1 = timeSuggestion1.getUnixEpochTime();

        // Initialize the strategy / device with a time set from a telephony suggestion.
        mScript.simulateTimePassing();
        long expectedSystemClockMillis1 = mScript.calculateTimeInMillisForNow(unixEpochTime1);
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion1)
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1)
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);

        // The Unix epoch time increment should be larger than the system clock update threshold so
        // we know it shouldn't be ignored for other reasons.
        long validUnixEpochTimeMillis = unixEpochTime1.getValue()
                + (2 * systemClockUpdateThreshold);

        // Now supply a new signal that has an obviously bogus reference time : older than the last
        // one.
        long referenceTimeBeforeLastSignalMillis = unixEpochTime1.getReferenceTimeMillis() - 1;
        TimestampedValue<Long> unixEpochTime2 = new TimestampedValue<>(
                referenceTimeBeforeLastSignalMillis, validUnixEpochTimeMillis);
        TelephonyTimeSuggestion timeSuggestion2 =
                createTelephonyTimeSuggestion(slotIndex, unixEpochTime2);
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion2)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);

        // Now supply a new signal that has an obviously bogus reference time : substantially in the
        // future.
        long referenceTimeInFutureMillis =
                unixEpochTime1.getReferenceTimeMillis() + Integer.MAX_VALUE + 1;
        TimestampedValue<Long> unixEpochTime3 = new TimestampedValue<>(
                referenceTimeInFutureMillis, validUnixEpochTimeMillis);
        TelephonyTimeSuggestion timeSuggestion3 =
                createTelephonyTimeSuggestion(slotIndex, unixEpochTime3);
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion3)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);

        // Just to prove validUnixEpochTimeMillis is valid.
        long validReferenceTimeMillis = unixEpochTime1.getReferenceTimeMillis() + 100;
        TimestampedValue<Long> unixEpochTime4 = new TimestampedValue<>(
                validReferenceTimeMillis, validUnixEpochTimeMillis);
        long expectedSystemClockMillis4 = mScript.calculateTimeInMillisForNow(unixEpochTime4);
        TelephonyTimeSuggestion timeSuggestion4 =
                createTelephonyTimeSuggestion(slotIndex, unixEpochTime4);
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion4)
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis4)
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion4);
    }

    @Test
    public void telephonyTimeSuggestion_ignoredWhenReferencedTimeIsInThePast() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true);

        int slotIndex = ARBITRARY_SLOT_INDEX;
        Instant suggestedTime = TIME_LOWER_BOUND.minus(Duration.ofDays(1));

        TelephonyTimeSuggestion timeSuggestion =
                mScript.generateTelephonyTimeSuggestion(
                        slotIndex, suggestedTime);

        mScript.simulateTelephonyTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, null);
    }

    @Test
    public void testSuggestTelephonyTime_timeDetectionToggled() {
        final int clockIncrementMillis = 100;
        final int systemClockUpdateThreshold = 2000;
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeThresholds(systemClockUpdateThreshold)
                .pokeAutoTimeDetectionEnabled(false);

        int slotIndex = ARBITRARY_SLOT_INDEX;
        Instant testTime = ARBITRARY_TEST_TIME;
        TelephonyTimeSuggestion timeSuggestion1 =
                mScript.generateTelephonyTimeSuggestion(slotIndex, testTime);
        TimestampedValue<Long> unixEpochTime1 = timeSuggestion1.getUnixEpochTime();

        // Simulate time passing.
        mScript.simulateTimePassing(clockIncrementMillis);

        // Simulate the time signal being received. It should not be used because auto time
        // detection is off but it should be recorded.
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion1)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);

        // Simulate more time passing.
        mScript.simulateTimePassing(clockIncrementMillis);

        long expectedSystemClockMillis1 = mScript.calculateTimeInMillisForNow(unixEpochTime1);

        // Turn on auto time detection.
        mScript.simulateAutoTimeDetectionToggle()
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1)
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);

        // Turn off auto time detection.
        mScript.simulateAutoTimeDetectionToggle()
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);

        // Receive another valid time signal.
        // It should be on the threshold and accounting for the clock increments.
        TelephonyTimeSuggestion timeSuggestion2 = mScript.generateTelephonyTimeSuggestion(
                slotIndex, mScript.peekSystemClockMillis() + systemClockUpdateThreshold);

        // Simulate more time passing.
        mScript.simulateTimePassing(clockIncrementMillis);

        long expectedSystemClockMillis2 =
                mScript.calculateTimeInMillisForNow(timeSuggestion2.getUnixEpochTime());

        // The new time, though valid, should not be set in the system clock because auto time is
        // disabled.
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion2)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion2);

        // Turn on auto time detection.
        mScript.simulateAutoTimeDetectionToggle()
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis2)
                .assertLatestTelephonySuggestion(slotIndex, timeSuggestion2);
    }

    @Test
    public void testSuggestTelephonyTime_maxSuggestionAge() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true);

        int slotIndex = ARBITRARY_SLOT_INDEX;
        Instant testTime = ARBITRARY_TEST_TIME;
        TelephonyTimeSuggestion telephonySuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, testTime);

        mScript.simulateTimePassing();

        long expectedSystemClockMillis =
                mScript.calculateTimeInMillisForNow(telephonySuggestion.getUnixEpochTime());
        mScript.simulateTelephonyTimeSuggestion(telephonySuggestion)
                .verifySystemClockWasSetAndResetCallTracking(
                        expectedSystemClockMillis  /* expectedNetworkBroadcast */)
                .assertLatestTelephonySuggestion(slotIndex, telephonySuggestion);

        // Look inside and check what the strategy considers the current best telephony suggestion.
        assertEquals(telephonySuggestion, mScript.peekBestTelephonySuggestion());

        // Simulate time passing, long enough that telephonySuggestion is now too old.
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS);

        // Look inside and check what the strategy considers the current best telephony suggestion.
        // It should still be the, it's just no longer used.
        assertNull(mScript.peekBestTelephonySuggestion());
        mScript.assertLatestTelephonySuggestion(slotIndex, telephonySuggestion);
    }

    @Test
    public void testSuggestManualTime_autoTimeDisabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(false);

        ManualTimeSuggestion timeSuggestion =
                mScript.generateManualTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing();

        long expectedSystemClockMillis =
                mScript.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
        mScript.simulateManualTimeSuggestion(timeSuggestion, true /* expectedResult */)
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
    }

    @Test
    public void testSuggestManualTime_retainsAutoSignal() {
        // Configure the start state.
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true);

        int slotIndex = ARBITRARY_SLOT_INDEX;

        // Simulate a telephony suggestion.
        Instant testTime = ARBITRARY_TEST_TIME;
        TelephonyTimeSuggestion telephonyTimeSuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, testTime);

        // Simulate the passage of time.
        mScript.simulateTimePassing();

        long expectedAutoClockMillis =
                mScript.calculateTimeInMillisForNow(telephonyTimeSuggestion.getUnixEpochTime());
        mScript.simulateTelephonyTimeSuggestion(telephonyTimeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(expectedAutoClockMillis)
                .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);

        // Simulate the passage of time.
        mScript.simulateTimePassing();

        // Switch to manual.
        mScript.simulateAutoTimeDetectionToggle()
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);

        // Simulate the passage of time.
        mScript.simulateTimePassing();

        // Simulate a manual suggestion 1 day different from the auto suggestion.
        Instant manualTime = testTime.plus(Duration.ofDays(1));
        ManualTimeSuggestion manualTimeSuggestion =
                mScript.generateManualTimeSuggestion(manualTime);
        mScript.simulateTimePassing();

        long expectedManualClockMillis =
                mScript.calculateTimeInMillisForNow(manualTimeSuggestion.getUnixEpochTime());
        mScript.simulateManualTimeSuggestion(manualTimeSuggestion, true /* expectedResult */)
                .verifySystemClockWasSetAndResetCallTracking(expectedManualClockMillis)
                .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);

        // Simulate the passage of time.
        mScript.simulateTimePassing();

        // Switch back to auto.
        mScript.simulateAutoTimeDetectionToggle();

        expectedAutoClockMillis =
                mScript.calculateTimeInMillisForNow(telephonyTimeSuggestion.getUnixEpochTime());
        mScript.verifySystemClockWasSetAndResetCallTracking(expectedAutoClockMillis)
                .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);

        // Switch back to manual - nothing should happen to the clock.
        mScript.simulateAutoTimeDetectionToggle()
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);
    }

    @Test
    public void manualTimeSuggestion_isIgnored_whenAutoTimeEnabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true);

        ManualTimeSuggestion timeSuggestion =
                mScript.generateManualTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing()
                .simulateManualTimeSuggestion(timeSuggestion, false /* expectedResult */)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void manualTimeSuggestion_ignoresTimeLowerBound() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(false);
        Instant suggestedTime = TIME_LOWER_BOUND.minus(Duration.ofDays(1));

        ManualTimeSuggestion timeSuggestion =
                mScript.generateManualTimeSuggestion(suggestedTime);

        mScript.simulateManualTimeSuggestion(timeSuggestion, true /* expectedResult */)
                .verifySystemClockWasSetAndResetCallTracking(suggestedTime.toEpochMilli());
    }

    @Test
    public void testSuggestNetworkTime_autoTimeEnabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_NETWORK)
                .pokeAutoTimeDetectionEnabled(true);

        NetworkTimeSuggestion timeSuggestion =
                mScript.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing();

        long expectedSystemClockMillis =
                mScript.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
        mScript.simulateNetworkTimeSuggestion(timeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
    }

    @Test
    public void testSuggestNetworkTime_autoTimeDisabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_NETWORK)
                .pokeAutoTimeDetectionEnabled(false);

        NetworkTimeSuggestion timeSuggestion =
                mScript.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing()
                .simulateNetworkTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void networkTimeSuggestion_ignoredWhenReferencedTimeIsInThePast() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_NETWORK)
                .pokeAutoTimeDetectionEnabled(true);

        Instant suggestedTime = TIME_LOWER_BOUND.minus(Duration.ofDays(1));
        NetworkTimeSuggestion timeSuggestion = mScript
                .generateNetworkTimeSuggestion(suggestedTime);

        mScript.simulateNetworkTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestNetworkSuggestion(null);
    }

    @Test
    public void testSuggestGnssTime_autoTimeEnabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_GNSS)
                .pokeAutoTimeDetectionEnabled(true);

        GnssTimeSuggestion timeSuggestion =
                mScript.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing();

        long expectedSystemClockMillis =
                mScript.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
        mScript.simulateGnssTimeSuggestion(timeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
    }

    @Test
    public void testSuggestGnssTime_autoTimeDisabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_GNSS)
                .pokeAutoTimeDetectionEnabled(false);

        GnssTimeSuggestion timeSuggestion =
                mScript.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing()
                .simulateGnssTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void testSuggestExternalTime_autoTimeEnabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_EXTERNAL)
                .pokeAutoTimeDetectionEnabled(true);

        ExternalTimeSuggestion timeSuggestion =
                mScript.generateExternalTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing();

        long expectedSystemClockMillis =
                mScript.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
        mScript.simulateExternalTimeSuggestion(timeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
    }

    @Test
    public void testSuggestExternalTime_autoTimeDisabled() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_EXTERNAL)
                .pokeAutoTimeDetectionEnabled(false);

        ExternalTimeSuggestion timeSuggestion =
                mScript.generateExternalTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateTimePassing()
                .simulateExternalTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void externalTimeSuggestion_ignoredWhenReferencedTimeIsInThePast() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoOriginPriorities(ORIGIN_EXTERNAL)
                .pokeAutoTimeDetectionEnabled(true);

        Instant suggestedTime = TIME_LOWER_BOUND.minus(Duration.ofDays(1));
        ExternalTimeSuggestion timeSuggestion = mScript
                .generateExternalTimeSuggestion(suggestedTime);

        mScript.simulateExternalTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking()
                .assertLatestExternalSuggestion(null);
    }

    @Test
    public void highPrioritySuggestionsBeatLowerPrioritySuggestions_telephonyNetworkOrigins() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK);

        // Three obviously different times that could not be mistaken for each other.
        Instant networkTime1 = ARBITRARY_TEST_TIME;
        Instant networkTime2 = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30));
        Instant telephonyTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60));
        // A small increment used to simulate the passage of time, but not enough to interfere with
        // macro-level time changes associated with suggestion age.
        final long smallTimeIncrementMillis = 101;

        // A network suggestion is made. It should be used because there is no telephony suggestion.
        NetworkTimeSuggestion networkTimeSuggestion1 =
                mScript.generateNetworkTimeSuggestion(networkTime1);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateNetworkTimeSuggestion(networkTimeSuggestion1)
                .verifySystemClockWasSetAndResetCallTracking(
                        mScript.calculateTimeInMillisForNow(
                                networkTimeSuggestion1.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, null)
                .assertLatestNetworkSuggestion(networkTimeSuggestion1);
        assertEquals(networkTimeSuggestion1, mScript.peekLatestValidNetworkSuggestion());
        assertNull("No telephony suggestions were made:", mScript.peekBestTelephonySuggestion());

        // Simulate a little time passing.
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Now a telephony suggestion is made. Telephony suggestions are prioritized over network
        // suggestions so it should "win".
        TelephonyTimeSuggestion telephonyTimeSuggestion =
                mScript.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, telephonyTime);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateTelephonyTimeSuggestion(telephonyTimeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(
                        mScript.calculateTimeInMillisForNow(
                                telephonyTimeSuggestion.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion)
                .assertLatestNetworkSuggestion(networkTimeSuggestion1);
        assertEquals(networkTimeSuggestion1, mScript.peekLatestValidNetworkSuggestion());
        assertEquals(telephonyTimeSuggestion, mScript.peekBestTelephonySuggestion());

        // Simulate some significant time passing: half the time allowed before a time signal
        // becomes "too old to use".
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS / 2)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Now another network suggestion is made. Telephony suggestions are prioritized over
        // network suggestions so the latest telephony suggestion should still "win".
        NetworkTimeSuggestion networkTimeSuggestion2 =
                mScript.generateNetworkTimeSuggestion(networkTime2);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateNetworkTimeSuggestion(networkTimeSuggestion2)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Check internal state.
        mScript.assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion)
                .assertLatestNetworkSuggestion(networkTimeSuggestion2);
        assertEquals(networkTimeSuggestion2, mScript.peekLatestValidNetworkSuggestion());
        assertEquals(telephonyTimeSuggestion, mScript.peekBestTelephonySuggestion());

        // Simulate some significant time passing: half the time allowed before a time signal
        // becomes "too old to use". This should mean that telephonyTimeSuggestion is now too old to
        // be used but networkTimeSuggestion2 is not.
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS / 2);

        // NOTE: The TimeDetectorStrategyImpl doesn't set an alarm for the point when the last
        // suggestion it used becomes too old: it requires a new suggestion or an auto-time toggle
        // to re-run the detection logic. This may change in future but until then we rely on a
        // steady stream of suggestions to re-evaluate.
        mScript.verifySystemClockWasNotSetAndResetCallTracking();

        // Check internal state.
        mScript.assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion)
                .assertLatestNetworkSuggestion(networkTimeSuggestion2);
        assertEquals(networkTimeSuggestion2, mScript.peekLatestValidNetworkSuggestion());
        assertNull(
                "Telephony suggestion should be expired:",
                mScript.peekBestTelephonySuggestion());

        // Toggle auto-time off and on to force the detection logic to run.
        mScript.simulateAutoTimeDetectionToggle()
                .simulateTimePassing(smallTimeIncrementMillis)
                .simulateAutoTimeDetectionToggle();

        // Verify the latest network time now wins.
        mScript.verifySystemClockWasSetAndResetCallTracking(
                mScript.calculateTimeInMillisForNow(networkTimeSuggestion2.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion)
                .assertLatestNetworkSuggestion(networkTimeSuggestion2);
        assertEquals(networkTimeSuggestion2, mScript.peekLatestValidNetworkSuggestion());
        assertNull(
                "Telephony suggestion should still be expired:",
                mScript.peekBestTelephonySuggestion());
    }

    @Test
    public void highPrioritySuggestionsBeatLowerPrioritySuggestions_networkGnssOrigins() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_NETWORK, ORIGIN_GNSS);

        // Three obviously different times that could not be mistaken for each other.
        Instant gnssTime1 = ARBITRARY_TEST_TIME;
        Instant gnssTime2 = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30));
        Instant networkTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60));
        // A small increment used to simulate the passage of time, but not enough to interfere with
        // macro-level time changes associated with suggestion age.
        final long smallTimeIncrementMillis = 101;

        // A gnss suggestion is made. It should be used because there is no network suggestion.
        GnssTimeSuggestion gnssTimeSuggestion1 =
                mScript.generateGnssTimeSuggestion(gnssTime1);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateGnssTimeSuggestion(gnssTimeSuggestion1)
                .verifySystemClockWasSetAndResetCallTracking(
                        mScript.calculateTimeInMillisForNow(
                                gnssTimeSuggestion1.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(null)
                .assertLatestGnssSuggestion(gnssTimeSuggestion1);
        assertEquals(gnssTimeSuggestion1, mScript.peekLatestValidGnssSuggestion());
        assertNull("No network suggestions were made:", mScript.peekLatestValidNetworkSuggestion());

        // Simulate a little time passing.
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Now a network suggestion is made. Network suggestions are prioritized over gnss
        // suggestions so it should "win".
        NetworkTimeSuggestion networkTimeSuggestion =
                mScript.generateNetworkTimeSuggestion(networkTime);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateNetworkTimeSuggestion(networkTimeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(
                        mScript.calculateTimeInMillisForNow(
                                networkTimeSuggestion.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestGnssSuggestion(gnssTimeSuggestion1);
        assertEquals(gnssTimeSuggestion1, mScript.peekLatestValidGnssSuggestion());
        assertEquals(networkTimeSuggestion, mScript.peekLatestValidNetworkSuggestion());

        // Simulate some significant time passing: half the time allowed before a time signal
        // becomes "too old to use".
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS / 2)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Now another gnss suggestion is made. Network suggestions are prioritized over
        // gnss suggestions so the latest network suggestion should still "win".
        GnssTimeSuggestion gnssTimeSuggestion2 =
                mScript.generateGnssTimeSuggestion(gnssTime2);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateGnssTimeSuggestion(gnssTimeSuggestion2)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestGnssSuggestion(gnssTimeSuggestion2);
        assertEquals(gnssTimeSuggestion2, mScript.peekLatestValidGnssSuggestion());
        assertEquals(networkTimeSuggestion, mScript.peekLatestValidNetworkSuggestion());

        // Simulate some significant time passing: half the time allowed before a time signal
        // becomes "too old to use". This should mean that telephonyTimeSuggestion is now too old to
        // be used but networkTimeSuggestion2 is not.
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS / 2);

        // NOTE: The TimeDetectorStrategyImpl doesn't set an alarm for the point when the last
        // suggestion it used becomes too old: it requires a new suggestion or an auto-time toggle
        // to re-run the detection logic. This may change in future but until then we rely on a
        // steady stream of suggestions to re-evaluate.
        mScript.verifySystemClockWasNotSetAndResetCallTracking();

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestGnssSuggestion(gnssTimeSuggestion2);
        assertEquals(gnssTimeSuggestion2, mScript.peekLatestValidGnssSuggestion());
        assertNull(
                "Network suggestion should be expired:",
                mScript.peekLatestValidNetworkSuggestion());

        // Toggle auto-time off and on to force the detection logic to run.
        mScript.simulateAutoTimeDetectionToggle()
                .simulateTimePassing(smallTimeIncrementMillis)
                .simulateAutoTimeDetectionToggle();

        // Verify the latest gnss time now wins.
        mScript.verifySystemClockWasSetAndResetCallTracking(
                mScript.calculateTimeInMillisForNow(gnssTimeSuggestion2.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestGnssSuggestion(gnssTimeSuggestion2);
        assertEquals(gnssTimeSuggestion2, mScript.peekLatestValidGnssSuggestion());
        assertNull(
                "Network suggestion should still be expired:",
                mScript.peekLatestValidNetworkSuggestion());
    }

    @Test
    public void highPrioritySuggestionsBeatLowerPrioritySuggestions_networkExternalOrigins() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_NETWORK, ORIGIN_EXTERNAL);

        // Three obviously different times that could not be mistaken for each other.
        Instant externalTime1 = ARBITRARY_TEST_TIME;
        Instant externalTime2 = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30));
        Instant networkTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60));
        // A small increment used to simulate the passage of time, but not enough to interfere with
        // macro-level time changes associated with suggestion age.
        final long smallTimeIncrementMillis = 101;

        // A external suggestion is made. It should be used because there is no network suggestion.
        ExternalTimeSuggestion externalTimeSuggestion1 =
                mScript.generateExternalTimeSuggestion(externalTime1);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateExternalTimeSuggestion(externalTimeSuggestion1)
                .verifySystemClockWasSetAndResetCallTracking(
                        mScript.calculateTimeInMillisForNow(
                                externalTimeSuggestion1.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(null)
                .assertLatestExternalSuggestion(externalTimeSuggestion1);
        assertEquals(externalTimeSuggestion1, mScript.peekLatestValidExternalSuggestion());
        assertNull("No network suggestions were made:", mScript.peekLatestValidNetworkSuggestion());

        // Simulate a little time passing.
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Now a network suggestion is made. Network suggestions are prioritized over external
        // suggestions so it should "win".
        NetworkTimeSuggestion networkTimeSuggestion =
                mScript.generateNetworkTimeSuggestion(networkTime);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateNetworkTimeSuggestion(networkTimeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(
                        mScript.calculateTimeInMillisForNow(
                                networkTimeSuggestion.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestExternalSuggestion(externalTimeSuggestion1);
        assertEquals(externalTimeSuggestion1, mScript.peekLatestValidExternalSuggestion());
        assertEquals(networkTimeSuggestion, mScript.peekLatestValidNetworkSuggestion());

        // Simulate some significant time passing: half the time allowed before a time signal
        // becomes "too old to use".
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS / 2)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Now another external suggestion is made. Network suggestions are prioritized over
        // external suggestions so the latest network suggestion should still "win".
        ExternalTimeSuggestion externalTimeSuggestion2 =
                mScript.generateExternalTimeSuggestion(externalTime2);
        mScript.simulateTimePassing(smallTimeIncrementMillis)
                .simulateExternalTimeSuggestion(externalTimeSuggestion2)
                .verifySystemClockWasNotSetAndResetCallTracking();

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestExternalSuggestion(externalTimeSuggestion2);
        assertEquals(externalTimeSuggestion2, mScript.peekLatestValidExternalSuggestion());
        assertEquals(networkTimeSuggestion, mScript.peekLatestValidNetworkSuggestion());

        // Simulate some significant time passing: half the time allowed before a time signal
        // becomes "too old to use". This should mean that networkTimeSuggestion is now too old to
        // be used but externalTimeSuggestion2 is not.
        mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS / 2);

        // NOTE: The TimeDetectorStrategyImpl doesn't set an alarm for the point when the last
        // suggestion it used becomes too old: it requires a new suggestion or an auto-time toggle
        // to re-run the detection logic. This may change in future but until then we rely on a
        // steady stream of suggestions to re-evaluate.
        mScript.verifySystemClockWasNotSetAndResetCallTracking();

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestExternalSuggestion(externalTimeSuggestion2);
        assertEquals(externalTimeSuggestion2, mScript.peekLatestValidExternalSuggestion());
        assertNull(
                "Network suggestion should be expired:",
                mScript.peekLatestValidNetworkSuggestion());

        // Toggle auto-time off and on to force the detection logic to run.
        mScript.simulateAutoTimeDetectionToggle()
                .simulateTimePassing(smallTimeIncrementMillis)
                .simulateAutoTimeDetectionToggle();

        // Verify the latest external time now wins.
        mScript.verifySystemClockWasSetAndResetCallTracking(
                mScript.calculateTimeInMillisForNow(externalTimeSuggestion2.getUnixEpochTime()));

        // Check internal state.
        mScript.assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestExternalSuggestion(externalTimeSuggestion2);
        assertEquals(externalTimeSuggestion2, mScript.peekLatestValidExternalSuggestion());
        assertNull(
                "Network suggestion should still be expired:",
                mScript.peekLatestValidNetworkSuggestion());
    }

    @Test
    public void whenAllTimeSuggestionsAreAvailable_higherPriorityWins_lowerPriorityComesFirst() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK, ORIGIN_EXTERNAL,
                          ORIGIN_GNSS);

        Instant networkTime = ARBITRARY_TEST_TIME;
        Instant externalTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(15));
        Instant gnssTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30));
        Instant telephonyTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60));

        NetworkTimeSuggestion networkTimeSuggestion =
                mScript.generateNetworkTimeSuggestion(networkTime);
        ExternalTimeSuggestion externalTimeSuggestion =
                mScript.generateExternalTimeSuggestion(externalTime);
        GnssTimeSuggestion gnssTimeSuggestion =
                mScript.generateGnssTimeSuggestion(gnssTime);
        TelephonyTimeSuggestion telephonyTimeSuggestion =
                mScript.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, telephonyTime);

        mScript.simulateNetworkTimeSuggestion(networkTimeSuggestion)
                .simulateExternalTimeSuggestion(externalTimeSuggestion)
                .simulateGnssTimeSuggestion(gnssTimeSuggestion)
                .simulateTelephonyTimeSuggestion(telephonyTimeSuggestion)
                .assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestExternalSuggestion(externalTimeSuggestion)
                .assertLatestGnssSuggestion(gnssTimeSuggestion)
                .assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(telephonyTime.toEpochMilli());
    }

    @Test
    public void whenAllTimeSuggestionsAreAvailable_higherPriorityWins_higherPriorityComesFirst() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK,
                        ORIGIN_EXTERNAL, ORIGIN_GNSS);

        Instant networkTime = ARBITRARY_TEST_TIME;
        Instant telephonyTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30));
        Instant externalTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(50));
        Instant gnssTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60));

        NetworkTimeSuggestion networkTimeSuggestion =
                mScript.generateNetworkTimeSuggestion(networkTime);
        TelephonyTimeSuggestion telephonyTimeSuggestion =
                mScript.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, telephonyTime);
        GnssTimeSuggestion gnssTimeSuggestion =
                mScript.generateGnssTimeSuggestion(gnssTime);
        ExternalTimeSuggestion externalTimeSuggestion =
                mScript.generateExternalTimeSuggestion(externalTime);

        mScript.simulateTelephonyTimeSuggestion(telephonyTimeSuggestion)
                .simulateNetworkTimeSuggestion(networkTimeSuggestion)
                .simulateGnssTimeSuggestion(gnssTimeSuggestion)
                .simulateExternalTimeSuggestion(externalTimeSuggestion)
                .assertLatestNetworkSuggestion(networkTimeSuggestion)
                .assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion)
                .assertLatestGnssSuggestion(gnssTimeSuggestion)
                .assertLatestExternalSuggestion(externalTimeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(telephonyTime.toEpochMilli());
    }

    @Test
    public void whenHighestPrioritySuggestionIsNotAvailable_fallbacksToNext() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK);

        NetworkTimeSuggestion timeSuggestion =
                mScript.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateNetworkTimeSuggestion(timeSuggestion)
                .assertLatestNetworkSuggestion(timeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(ARBITRARY_TEST_TIME.toEpochMilli());
    }

    @Test
    public void whenHigherPrioritySuggestionsAreNotAvailable_fallbacksToNext() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK,
                                ORIGIN_EXTERNAL, ORIGIN_GNSS);

        GnssTimeSuggestion timeSuggestion =
                mScript.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateGnssTimeSuggestion(timeSuggestion)
                .assertLatestGnssSuggestion(timeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(ARBITRARY_TEST_TIME.toEpochMilli());
    }

    @Test
    public void suggestionsFromTelephonyOriginNotInPriorityList_areIgnored() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_NETWORK);

        int slotIndex = ARBITRARY_SLOT_INDEX;
        Instant testTime = ARBITRARY_TEST_TIME;
        TelephonyTimeSuggestion timeSuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, testTime);

        mScript.simulateTelephonyTimeSuggestion(timeSuggestion)
                .assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void suggestionsFromNetworkOriginNotInPriorityList_areIgnored() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY);

        NetworkTimeSuggestion timeSuggestion = mScript.generateNetworkTimeSuggestion(
                ARBITRARY_TEST_TIME);

        mScript.simulateNetworkTimeSuggestion(timeSuggestion)
                .assertLatestNetworkSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void suggestionsFromGnssOriginNotInPriorityList_areIgnored() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY);

        GnssTimeSuggestion timeSuggestion = mScript.generateGnssTimeSuggestion(
                ARBITRARY_TEST_TIME);

        mScript.simulateGnssTimeSuggestion(timeSuggestion)
                .assertLatestGnssSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void suggestionsFromExternalOriginNotInPriorityList_areIgnored() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY);

        ExternalTimeSuggestion timeSuggestion = mScript.generateExternalTimeSuggestion(
                ARBITRARY_TEST_TIME);

        mScript.simulateExternalTimeSuggestion(timeSuggestion)
                .assertLatestExternalSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void autoOriginPrioritiesList_doesNotAffectManualSuggestion() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(false)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY);

        ManualTimeSuggestion timeSuggestion =
                mScript.generateManualTimeSuggestion(ARBITRARY_TEST_TIME);

        mScript.simulateManualTimeSuggestion(timeSuggestion, true /* expectedResult */)
                .verifySystemClockWasSetAndResetCallTracking(ARBITRARY_TEST_TIME.toEpochMilli());
    }

    @Test
    public void manualY2038SuggestionsAreRejectedOnAffectedDevices() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(false)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY)
                .pokeDeviceHasY2038Issues(true);

        Instant y2038IssueTime = Instant.ofEpochMilli((1L + Integer.MAX_VALUE) * 1000L);
        ManualTimeSuggestion timeSuggestion = mScript.generateManualTimeSuggestion(y2038IssueTime);
        mScript.simulateManualTimeSuggestion(timeSuggestion, false /* expectedResult */)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void telephonyY2038SuggestionsAreRejectedOnAffectedDevices() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY)
                .pokeDeviceHasY2038Issues(true);

        final int slotIndex = 0;
        Instant y2038IssueTime = Instant.ofEpochMilli((1L + Integer.MAX_VALUE) * 1000L);
        TelephonyTimeSuggestion timeSuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, y2038IssueTime);
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion)
                .verifySystemClockWasNotSetAndResetCallTracking();
    }

    @Test
    public void telephonyY2038SuggestionsAreNotRejectedOnUnaffectedDevices() {
        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
                .pokeAutoTimeDetectionEnabled(true)
                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY)
                .pokeDeviceHasY2038Issues(false);

        final int slotIndex = 0;
        Instant y2038IssueTime = Instant.ofEpochMilli((1L + Integer.MAX_VALUE) * 1000L);
        TelephonyTimeSuggestion timeSuggestion =
                mScript.generateTelephonyTimeSuggestion(slotIndex, y2038IssueTime);
        mScript.simulateTelephonyTimeSuggestion(timeSuggestion)
                .verifySystemClockWasSetAndResetCallTracking(y2038IssueTime.toEpochMilli());
    }

    /**
     * A fake implementation of {@link TimeDetectorStrategyImpl.Environment}. Besides tracking
     * changes and behaving like the real thing should, it also asserts preconditions.
     */
    private static class FakeEnvironment implements TimeDetectorStrategyImpl.Environment {
        private boolean mAutoTimeDetectionEnabled;
        private boolean mWakeLockAcquired;
        private long mElapsedRealtimeMillis;
        private long mSystemClockMillis;
        private int mSystemClockUpdateThresholdMillis = 2000;
        private int[] mAutoOriginPriorities = PROVIDERS_PRIORITY;
        private ConfigurationChangeListener mConfigChangeListener;
        private boolean mDeviceHas2038Issues = false;

        // Tracking operations.
        private boolean mSystemClockWasSet;

        @Override
        public void setConfigChangeListener(ConfigurationChangeListener listener) {
            mConfigChangeListener = Objects.requireNonNull(listener);
        }

        @Override
        public int systemClockUpdateThresholdMillis() {
            return mSystemClockUpdateThresholdMillis;
        }

        @Override
        public boolean isAutoTimeDetectionEnabled() {
            return mAutoTimeDetectionEnabled;
        }

        @Override
        public Instant autoTimeLowerBound() {
            return TIME_LOWER_BOUND;
        }

        @Override
        public int[] autoOriginPriorities() {
            return mAutoOriginPriorities;
        }

        @Override
        public ConfigurationInternal configurationInternal(int userId) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void acquireWakeLock() {
            if (mWakeLockAcquired) {
                fail("Wake lock already acquired");
            }
            mWakeLockAcquired = true;
        }

        @Override
        public long elapsedRealtimeMillis() {
            return mElapsedRealtimeMillis;
        }

        @Override
        public long systemClockMillis() {
            return mSystemClockMillis;
        }

        @Override
        public void setSystemClock(long newTimeMillis) {
            assertWakeLockAcquired();
            mSystemClockWasSet = true;
            mSystemClockMillis = newTimeMillis;
        }

        @Override
        public void releaseWakeLock() {
            assertWakeLockAcquired();
            mWakeLockAcquired = false;
        }

        public void setDeviceHas2038Issues(boolean hasIssues) {
            mDeviceHas2038Issues = hasIssues;
        }

        @Override
        public boolean deviceHasY2038Issue() {
            return mDeviceHas2038Issues;
        }

        // Methods below are for managing the fake's behavior.

        void pokeSystemClockUpdateThreshold(int thresholdMillis) {
            mSystemClockUpdateThresholdMillis = thresholdMillis;
        }

        void pokeElapsedRealtimeMillis(long elapsedRealtimeMillis) {
            mElapsedRealtimeMillis = elapsedRealtimeMillis;
        }

        void pokeSystemClockMillis(long systemClockMillis) {
            mSystemClockMillis = systemClockMillis;
        }

        void pokeAutoTimeDetectionEnabled(boolean enabled) {
            mAutoTimeDetectionEnabled = enabled;
        }

        void pokeAutoOriginPriorities(@Origin int[] autoOriginPriorities) {
            mAutoOriginPriorities = autoOriginPriorities;
        }

        long peekElapsedRealtimeMillis() {
            return mElapsedRealtimeMillis;
        }

        long peekSystemClockMillis() {
            return mSystemClockMillis;
        }

        void simulateTimePassing(long incrementMillis) {
            mElapsedRealtimeMillis += incrementMillis;
            mSystemClockMillis += incrementMillis;
        }

        void simulateAutoTimeZoneDetectionToggle() {
            mAutoTimeDetectionEnabled = !mAutoTimeDetectionEnabled;
            mConfigChangeListener.onChange();
        }

        void verifySystemClockNotSet() {
            assertFalse(
                    String.format("System clock was manipulated and set to %s(=%s)",
                            Instant.ofEpochMilli(mSystemClockMillis), mSystemClockMillis),
                    mSystemClockWasSet);
        }

        void verifySystemClockWasSet(long expectedSystemClockMillis) {
            assertTrue(mSystemClockWasSet);
            assertEquals(expectedSystemClockMillis, mSystemClockMillis);
        }

        void resetCallTracking() {
            mSystemClockWasSet = false;
        }

        private void assertWakeLockAcquired() {
            assertTrue("The operation must be performed only after acquiring the wakelock",
                    mWakeLockAcquired);
        }
    }

    /**
     * A fluent helper class for tests.
     */
    private class Script {

        private final FakeEnvironment mFakeEnvironment;
        private final TimeDetectorStrategyImpl mTimeDetectorStrategy;

        Script() {
            mFakeEnvironment = new FakeEnvironment();
            mTimeDetectorStrategy = new TimeDetectorStrategyImpl(mFakeEnvironment);
        }

        Script pokeAutoTimeDetectionEnabled(boolean enabled) {
            mFakeEnvironment.pokeAutoTimeDetectionEnabled(enabled);
            return this;
        }

        Script pokeFakeClocks(TimestampedValue<Instant> timeInfo) {
            mFakeEnvironment.pokeElapsedRealtimeMillis(timeInfo.getReferenceTimeMillis());
            mFakeEnvironment.pokeSystemClockMillis(timeInfo.getValue().toEpochMilli());
            return this;
        }

        Script pokeThresholds(int systemClockUpdateThreshold) {
            mFakeEnvironment.pokeSystemClockUpdateThreshold(systemClockUpdateThreshold);
            return this;
        }

        Script pokeAutoOriginPriorities(@Origin int... autoOriginPriorities) {
            mFakeEnvironment.pokeAutoOriginPriorities(autoOriginPriorities);
            return this;
        }

        Script pokeDeviceHasY2038Issues(boolean hasIssues) {
            mFakeEnvironment.setDeviceHas2038Issues(hasIssues);
            return this;
        }

        long peekElapsedRealtimeMillis() {
            return mFakeEnvironment.peekElapsedRealtimeMillis();
        }

        long peekSystemClockMillis() {
            return mFakeEnvironment.peekSystemClockMillis();
        }

        Script simulateTelephonyTimeSuggestion(TelephonyTimeSuggestion timeSuggestion) {
            mTimeDetectorStrategy.suggestTelephonyTime(timeSuggestion);
            return this;
        }

        Script simulateManualTimeSuggestion(
                ManualTimeSuggestion timeSuggestion, boolean expectedResult) {
            String errorMessage = expectedResult
                    ? "Manual time suggestion was ignored, but expected to be accepted."
                    : "Manual time suggestion was accepted, but expected to be ignored.";
            assertEquals(
                    errorMessage,
                    expectedResult,
                    mTimeDetectorStrategy.suggestManualTime(timeSuggestion));
            return this;
        }

        Script simulateNetworkTimeSuggestion(NetworkTimeSuggestion timeSuggestion) {
            mTimeDetectorStrategy.suggestNetworkTime(timeSuggestion);
            return this;
        }

        Script simulateGnssTimeSuggestion(GnssTimeSuggestion timeSuggestion) {
            mTimeDetectorStrategy.suggestGnssTime(timeSuggestion);
            return this;
        }

        Script simulateExternalTimeSuggestion(ExternalTimeSuggestion timeSuggestion) {
            mTimeDetectorStrategy.suggestExternalTime(timeSuggestion);
            return this;
        }

        Script simulateAutoTimeDetectionToggle() {
            mFakeEnvironment.simulateAutoTimeZoneDetectionToggle();
            return this;
        }

        Script simulateTimePassing(long clockIncrementMillis) {
            mFakeEnvironment.simulateTimePassing(clockIncrementMillis);
            return this;
        }

        /**
         * Simulates time passing by an arbitrary (but relatively small) amount.
         */
        Script simulateTimePassing() {
            return simulateTimePassing(999);
        }

        Script verifySystemClockWasNotSetAndResetCallTracking() {
            mFakeEnvironment.verifySystemClockNotSet();
            mFakeEnvironment.resetCallTracking();
            return this;
        }

        Script verifySystemClockWasSetAndResetCallTracking(long expectedSystemClockMillis) {
            mFakeEnvironment.verifySystemClockWasSet(expectedSystemClockMillis);
            mFakeEnvironment.resetCallTracking();
            return this;
        }

        /**
         * White box test info: Asserts the latest suggestion for the slotIndex is as expected.
         */
        Script assertLatestTelephonySuggestion(int slotIndex, TelephonyTimeSuggestion expected) {
            assertEquals(
                    "Expected to see " + expected + " at slotIndex=" + slotIndex + ", but got "
                            + mTimeDetectorStrategy.getLatestTelephonySuggestion(slotIndex),
                    expected, mTimeDetectorStrategy.getLatestTelephonySuggestion(slotIndex));
            return this;
        }

        /**
         * White box test info: Asserts the latest network suggestion is as expected.
         */
        Script assertLatestNetworkSuggestion(NetworkTimeSuggestion expected) {
            assertEquals(expected, mTimeDetectorStrategy.getLatestNetworkSuggestion());
            return this;
        }

        /**
         * White box test info: Asserts the latest gnss suggestion is as expected.
         */
        Script assertLatestGnssSuggestion(GnssTimeSuggestion expected) {
            assertEquals(expected, mTimeDetectorStrategy.getLatestGnssSuggestion());
            return this;
        }

        /**
         * White box test info: Asserts the latest external suggestion is as expected.
         */
        Script assertLatestExternalSuggestion(ExternalTimeSuggestion expected) {
            assertEquals(expected, mTimeDetectorStrategy.getLatestExternalSuggestion());
            return this;
        }

        /**
         * White box test info: Returns the telephony suggestion that would be used, if any, given
         * the current elapsed real time clock and regardless of origin prioritization.
         */
        TelephonyTimeSuggestion peekBestTelephonySuggestion() {
            return mTimeDetectorStrategy.findBestTelephonySuggestionForTests();
        }

        /**
         * White box test info: Returns the network suggestion that would be used, if any, given the
         * current elapsed real time clock and regardless of origin prioritization.
         */
        NetworkTimeSuggestion peekLatestValidNetworkSuggestion() {
            return mTimeDetectorStrategy.findLatestValidNetworkSuggestionForTests();
        }

        /**
         * White box test info: Returns the gnss suggestion that would be used, if any, given the
         * current elapsed real time clock and regardless of origin prioritization.
         */
        GnssTimeSuggestion peekLatestValidGnssSuggestion() {
            return mTimeDetectorStrategy.findLatestValidGnssSuggestionForTests();
        }

        /**
         * White box test info: Returns the external suggestion that would be used, if any, given
         * the current elapsed real time clock and regardless of origin prioritization.
         */
        ExternalTimeSuggestion peekLatestValidExternalSuggestion() {
            return mTimeDetectorStrategy.findLatestValidExternalSuggestionForTests();
        }

        /**
         * Generates a ManualTimeSuggestion using the current elapsed realtime clock for the
         * reference time.
         */
        ManualTimeSuggestion generateManualTimeSuggestion(Instant suggestedTime) {
            TimestampedValue<Long> unixEpochTime =
                    new TimestampedValue<>(
                            mFakeEnvironment.peekElapsedRealtimeMillis(),
                            suggestedTime.toEpochMilli());
            return new ManualTimeSuggestion(unixEpochTime);
        }

        /**
         * Generates a {@link TelephonyTimeSuggestion} using the current elapsed realtime clock for
         * the reference time.
         */
        TelephonyTimeSuggestion generateTelephonyTimeSuggestion(int slotIndex, long timeMillis) {
            TimestampedValue<Long> time =
                    new TimestampedValue<>(peekElapsedRealtimeMillis(), timeMillis);
            return createTelephonyTimeSuggestion(slotIndex, time);
        }

        /**
         * Generates a {@link TelephonyTimeSuggestion} using the current elapsed realtime clock for
         * the reference time.
         */
        TelephonyTimeSuggestion generateTelephonyTimeSuggestion(
                int slotIndex, Instant suggestedTime) {
            if (suggestedTime == null) {
                return createTelephonyTimeSuggestion(slotIndex, null);
            }
            return generateTelephonyTimeSuggestion(slotIndex, suggestedTime.toEpochMilli());
        }

        /**
         * Generates a NetworkTimeSuggestion using the current elapsed realtime clock for the
         * reference time.
         */
        NetworkTimeSuggestion generateNetworkTimeSuggestion(Instant suggestedTime) {
            TimestampedValue<Long> unixEpochTime =
                    new TimestampedValue<>(
                            mFakeEnvironment.peekElapsedRealtimeMillis(),
                            suggestedTime.toEpochMilli());
            return new NetworkTimeSuggestion(unixEpochTime);
        }

        /**
         * Generates a GnssTimeSuggestion using the current elapsed realtime clock for the
         * reference time.
         */
        GnssTimeSuggestion generateGnssTimeSuggestion(Instant suggestedTime) {
            TimestampedValue<Long> unixEpochTime =
                    new TimestampedValue<>(
                            mFakeEnvironment.peekElapsedRealtimeMillis(),
                            suggestedTime.toEpochMilli());
            return new GnssTimeSuggestion(unixEpochTime);
        }

        /**
         * Generates a ExternalTimeSuggestion using the current elapsed realtime clock for the
         * reference time.
         */
        ExternalTimeSuggestion generateExternalTimeSuggestion(Instant suggestedTime) {
            return new ExternalTimeSuggestion(mFakeEnvironment.peekElapsedRealtimeMillis(),
                            suggestedTime.toEpochMilli());
        }

        /**
         * Calculates what the supplied time would be when adjusted for the movement of the fake
         * elapsed realtime clock.
         */
        long calculateTimeInMillisForNow(TimestampedValue<Long> unixEpochTime) {
            return TimeDetectorStrategy.getTimeAt(unixEpochTime, peekElapsedRealtimeMillis());
        }
    }

    private static TelephonyTimeSuggestion createTelephonyTimeSuggestion(int slotIndex,
            TimestampedValue<Long> unixEpochTime) {
        return new TelephonyTimeSuggestion.Builder(slotIndex)
                .setUnixEpochTime(unixEpochTime)
                .build();
    }

    private static Instant createUnixEpochTime(int year, int monthInYear, int day, int hourOfDay,
            int minute, int second) {
        return LocalDateTime.of(year, monthInYear, day, hourOfDay, minute, second)
                .toInstant(ZoneOffset.UTC);
    }
}
