/*
 * 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.devicepolicy;

import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE;
import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG;
import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_DUPLICATE_OR_OVERLAP;
import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE;
import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_NEW_FREEZE_PERIOD_TOO_LONG;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assert.fail;

import android.app.admin.FreezePeriod;
import android.app.admin.SystemUpdatePolicy;
import android.os.Parcel;
import android.util.Xml;

import androidx.test.runner.AndroidJUnit4;

import com.android.internal.util.FastXmlSerializer;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.MonthDay;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Unit tests for {@link android.app.admin.SystemUpdatePolicy}.
 *
 * <p>NOTE: Throughout this test, we use {@code "MM-DD"} format to denote dates without year.
 *
 * <p>Run this test with:
 *
 * {@code atest FrameworksServicesTests:com.android.server.devicepolicy.SystemUpdatePolicyTest}
 */
@RunWith(AndroidJUnit4.class)
public final class SystemUpdatePolicyTest {

    private static final int DUPLICATE_OR_OVERLAP = ERROR_DUPLICATE_OR_OVERLAP;
    private static final int TOO_LONG = ERROR_NEW_FREEZE_PERIOD_TOO_LONG;
    private static final int TOO_CLOSE = ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE;
    private static final int COMBINED_TOO_LONG = ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG;
    private static final int COMBINED_TOO_CLOSE = ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE;

    @Test
    public void testSimplePeriod() throws Exception {
        testFreezePeriodsSucceeds("01-01", "01-02");
        testFreezePeriodsSucceeds("01-31", "01-31");
        testFreezePeriodsSucceeds("11-01", "01-15");
        testFreezePeriodsSucceeds("02-01", "02-29"); // Leap year
        testFreezePeriodsSucceeds("02-01", "03-01");
        testFreezePeriodsSucceeds("12-01", "01-30"); // Wrapped Period
        testFreezePeriodsSucceeds("11-02", "01-30", "04-01", "04-30"); // Wrapped Period
    }

    @Test
    public void testCanonicalizationValidation() throws Exception {
        testFreezePeriodsSucceeds("03-01", "03-31", "09-01", "09-30");
        testFreezePeriodsSucceeds("06-01", "07-01", "09-01", "09-30");
        testFreezePeriodsSucceeds("10-01", "10-31", "12-31", "01-31");
        testFreezePeriodsSucceeds("01-01", "01-30", "04-01", "04-30");
        testFreezePeriodsSucceeds("01-01", "02-28", "05-01", "06-30", "09-01", "10-31");

        // One interval fully covers the other
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "03-01", "03-31", "03-15", "03-31");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "03-01", "03-31", "03-15", "03-16");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "11-15", "01-31", "12-01", "12-31");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "01-31", "01-01", "01-15");

        // Partial overlap
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "03-01", "03-31", "03-15", "01-01");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "11-15", "01-31", "12-01", "02-28");

        // No gap between two intervals
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "01-31", "01-31", "02-01", "02-01");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "12-15", "12-15", "02-01");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "12-15", "12-16", "02-01");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "01-15", "01-15", "02-01");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "01-15", "01-16", "02-01");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "01-01", "01-30", "12-01", "12-31");
        testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "12-31", "04-01", "04-01",
                "01-01", "01-30");
    }

    @Test
    public void testLengthValidation() throws Exception {
        testFreezePeriodsSucceeds("03-01", "03-31");
        testFreezePeriodsSucceeds("03-03", "03-03", "12-31", "01-01");
        testFreezePeriodsSucceeds("01-01", "03-31", "06-01", "08-29");
        // entire year
        testFreezePeriodsFails(TOO_LONG, "01-01", "12-31");
        // long period spanning across year end
        testFreezePeriodsSucceeds("11-01", "01-29");
        testFreezePeriodsFails(TOO_LONG, "11-01", "01-30");
        // Leap year handling
        testFreezePeriodsSucceeds("12-01", "02-28");
        testFreezePeriodsSucceeds("12-01", "02-29");
        testFreezePeriodsFails(TOO_LONG, "12-01", "03-01");
        // Regular long period
        testFreezePeriodsSucceeds("01-01", "03-31", "06-01", "08-29");
        testFreezePeriodsFails(TOO_LONG, "01-01", "03-31", "06-01", "08-30");
    }

    @Test
    public void testSeparationValidation() throws Exception {
        testFreezePeriodsSucceeds("01-01", "03-31", "06-01", "08-29");
        testFreezePeriodsFails(TOO_CLOSE, "01-01", "01-01", "01-03", "01-03");
        testFreezePeriodsFails(TOO_CLOSE, "03-01", "03-31", "05-01", "05-31");
        // Short interval spans across end of year
        testFreezePeriodsSucceeds("01-31", "03-01", "11-01", "12-01");
        testFreezePeriodsFails(TOO_CLOSE, "01-30", "03-01", "11-01", "12-01");
        // Short separation is after wrapped period
        testFreezePeriodsSucceeds("03-03", "03-31", "12-31", "01-01");
        testFreezePeriodsFails(TOO_CLOSE, "03-02", "03-31", "12-31", "01-01");
        // Short separation including Feb 29
        testFreezePeriodsSucceeds("12-01", "01-15", "03-17", "04-01");
        testFreezePeriodsFails(TOO_CLOSE, "12-01", "01-15", "03-16", "04-01");
        // Short separation including Feb 29
        testFreezePeriodsSucceeds("01-01", "02-28", "04-30", "06-01");
        testFreezePeriodsSucceeds("01-01", "02-29", "04-30", "06-01");
        testFreezePeriodsFails(TOO_CLOSE, "01-01", "03-01", "04-30", "06-01");
    }

    @Test
    public void testValidateTotalLengthWithPreviousPeriods() throws Exception {
        testPrevFreezePeriodSucceeds("2018-01-19", "2018-01-19", /* now */"2018-01-19",
                "07-01", "07-31", "10-01", "11-30");
        testPrevFreezePeriodSucceeds("2018-01-01", "2018-01-19", /* now */"2018-01-19",
                "01-01", "03-30");
        testPrevFreezePeriodSucceeds("2018-01-01", "2018-02-01", /* now */"2018-02-01",
                "11-01", "12-31");

        testPrevFreezePeriodSucceeds("2017-11-01", "2018-01-02", /* now */"2018-01-02",
                "01-01", "01-29");
        testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2018-01-02", "2018-01-02",
                "01-01", "01-30");
        testPrevFreezePeriodSucceeds("2017-11-01", "2018-01-02", /* now */"2018-01-01",
                "01-02", "01-29");
        testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2018-01-02", "2018-01-01",
                "01-02", "01-30");

        testPrevFreezePeriodSucceeds("2017-11-01", "2017-12-01", /* now */"2017-12-01",
                "11-15", "01-29");
        testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2017-12-01", "2017-12-01",
                "11-15", "01-30");

        testPrevFreezePeriodSucceeds("2017-11-01", "2018-01-01", /* now */"2018-01-01",
                "11-15", "01-29");
        testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2018-01-01", "2018-01-01",
                "11-15", "01-30");

        testPrevFreezePeriodSucceeds("2018-03-01", "2018-03-31", /* now */"2018-03-31",
                "04-01", "05-29");
        testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2018-03-01", "2018-03-31", "2018-03-31",
                "04-01", "05-30");

        // Leap year handing
        testPrevFreezePeriodSucceeds("2017-12-01", "2018-01-02", /* now */"2018-01-02",
                "01-01", "02-28");
        testPrevFreezePeriodSucceeds("2017-12-01", "2018-01-02", /* now */"2018-01-02",
                "01-01", "02-29");
        testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-12-01", "2018-01-02", "2018-01-02",
                "01-01", "03-01");

        testPrevFreezePeriodSucceeds("2016-01-01", "2016-02-28", /* now */"2016-02-28",
                "02-01", "03-31");
        testPrevFreezePeriodSucceeds("2016-01-01", "2016-02-28", /* now */"2016-02-29",
                "02-01", "03-31");
        testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2016-01-01", "2016-02-28", "2016-02-29",
                "02-01", "04-01");

    }

    @Test
    public void testValidateSeparationWithPreviousPeriods() throws Exception {
        testPrevFreezePeriodSucceeds("2018-01-01", "2018-01-02", /* now */"2018-03-04",
                "01-01", "03-30");
        testPrevFreezePeriodSucceeds("2018-01-01", "2018-01-02", /* now */"2018-01-19",
                "04-01", "06-29");
        testPrevFreezePeriodSucceeds("2017-01-01", "2017-03-30", /* now */"2018-12-01",
                "01-01", "03-30");

        testPrevFreezePeriodSucceeds("2018-01-01", "2018-02-01", "2018-02-01",
                "04-03", "06-01");
        testPrevFreezePeriodFails(COMBINED_TOO_CLOSE, "2018-01-01", "2018-02-01", "2018-02-01",
                "04-02", "06-01");

        testPrevFreezePeriodSucceeds("2018-04-01", "2018-06-01", "2018-08-01",
                "07-01", "08-30");
        testPrevFreezePeriodFails(COMBINED_TOO_CLOSE, "2018-04-01", "2018-06-01", "2018-07-30",
                "07-01", "08-30");


        testPrevFreezePeriodSucceeds("2018-03-01", "2018-04-01", "2018-06-01",
                "05-01", "07-01");
        testPrevFreezePeriodFails(COMBINED_TOO_CLOSE, "2018-03-01", "2018-04-01", "2018-05-31",
                "05-01", "07-01");
    }

    @Test
    public void testDistanceWithoutLeapYear() {
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2016, 12, 31), LocalDate.of(2016, 1, 1))).isEqualTo(364);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2017, 1, 1), LocalDate.of(2016, 1, 1))).isEqualTo(365);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2017, 2, 28), LocalDate.of(2016, 2, 29))).isEqualTo(365);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2016, 1, 1), LocalDate.of(2017, 1, 1))).isEqualTo(-365);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2016, 3, 1), LocalDate.of(2016, 2, 29))).isEqualTo(1);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2016, 3, 1), LocalDate.of(2016, 2, 28))).isEqualTo(1);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2016, 2, 29), LocalDate.of(2016, 2, 28))).isEqualTo(0);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2016, 2, 28), LocalDate.of(2016, 2, 28))).isEqualTo(0);

        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2016, 3, 1), LocalDate.of(2016, 1, 1))).isEqualTo(59);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2017, 3, 1), LocalDate.of(2017, 1, 1))).isEqualTo(59);

        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2040, 1, 1), LocalDate.of(2000, 1, 1))).isEqualTo(365 * 40);

        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2019, 3, 1), LocalDate.of(2017, 3, 1))).isEqualTo(365 * 2);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2018, 3, 1), LocalDate.of(2016, 3, 1))).isEqualTo(365 * 2);
        assertThat(FreezePeriod.distanceWithoutLeapYear(
        LocalDate.of(2017, 3, 1), LocalDate.of(2015, 3, 1))).isEqualTo(365 * 2);

    }

    @Test
    public void testInstallationOptionWithoutFreeze() {
        // Also duplicated at com.google.android.gts.deviceowner.SystemUpdatePolicyTest
        final long millis_2018_01_01 = toMillis(2018, 1, 1);

        SystemUpdatePolicy p = SystemUpdatePolicy.createAutomaticInstallPolicy();
        assertInstallationOption(SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, Long.MAX_VALUE,
                millis_2018_01_01, p);

        p = SystemUpdatePolicy.createPostponeInstallPolicy();
        assertInstallationOption(SystemUpdatePolicy.TYPE_POSTPONE, Long.MAX_VALUE,
                millis_2018_01_01, p);

        p = SystemUpdatePolicy.createWindowedInstallPolicy(120, 180); // 2:00 - 3:00
        // 00:00 is two hours before the next window
        assertInstallationOption(SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.HOURS.toMillis(2),
                millis_2018_01_01, p);
        // 02:00 is within the current maintenance window, and one hour until the window ends
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.HOURS.toMillis(1),
                millis_2018_01_01 + TimeUnit.HOURS.toMillis(2), p);
        // 04:00 is 22 hours from the window next day
        assertInstallationOption(SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.HOURS.toMillis(22),
                millis_2018_01_01 + TimeUnit.HOURS.toMillis(4), p);

        p = SystemUpdatePolicy.createWindowedInstallPolicy(22 * 60, 2 * 60); // 22:00 - 2:00
        // 21:00 is one hour from the next window
        assertInstallationOption(SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.HOURS.toMillis(1),
                millis_2018_01_01 + TimeUnit.HOURS.toMillis(21), p);
        // 00:00 is two hours from the end of current window
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.HOURS.toMillis(2),
                millis_2018_01_01, p);
        // 03:00 is 22 hours from the window today
        assertInstallationOption(SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.HOURS.toMillis(19),
                millis_2018_01_01 + TimeUnit.HOURS.toMillis(3), p);
    }

    @Test
    public void testInstallationOptionWithFreeze() throws Exception {
        final long millis_2016_02_29 = toMillis(2016, 2, 29);
        final long millis_2017_01_31 = toMillis(2017, 1, 31);
        final long millis_2017_02_28 = toMillis(2017, 2, 28);
        final long millis_2018_01_01 = toMillis(2018, 1, 1);
        final long millis_2018_08_01 = toMillis(2018, 8, 1);

        SystemUpdatePolicy p = SystemUpdatePolicy.createAutomaticInstallPolicy();
        setFreezePeriods(p, "01-01", "01-31");
        // Inside a freeze period
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(31),
                millis_2018_01_01, p);
        // Device is outside freeze between 2/28 to 12/31 inclusive
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.DAYS.toMillis(307),
                millis_2017_02_28, p);

        // Freeze period contains leap day Feb 29
        p = SystemUpdatePolicy.createPostponeInstallPolicy();
        setFreezePeriods(p, "02-01", "03-05");
        // Freezed until 3/5, note 2016 is a leap year
        assertInstallationOption(SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(6),
                millis_2016_02_29, p);
        // Freezed until 3/5, note 2017 is not a leap year
        assertInstallationOption(SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(6),
                millis_2017_02_28, p);
        // Next freeze is 2018/2/1
        assertInstallationOption(SystemUpdatePolicy.TYPE_POSTPONE, TimeUnit.DAYS.toMillis(31),
                millis_2018_01_01, p);

        // Freeze period start on or right after leap day
        p = SystemUpdatePolicy.createAutomaticInstallPolicy();
        setFreezePeriods(p, "03-01", "03-31");
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.DAYS.toMillis(1),
                millis_2016_02_29, p);
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.DAYS.toMillis(1),
                millis_2017_02_28, p);
        setFreezePeriods(p, "02-28", "03-05");
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(6),
                millis_2016_02_29, p);
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(6),
                millis_2017_02_28, p);

        // Freeze period end on or right after leap day
        p = SystemUpdatePolicy.createAutomaticInstallPolicy();
        setFreezePeriods(p, "02-01", "02-28");
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(1),
                millis_2016_02_29, p);
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(1),
                millis_2017_02_28, p);
        p = SystemUpdatePolicy.createAutomaticInstallPolicy();
        setFreezePeriods(p, "02-01", "03-01");
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(2),
                millis_2016_02_29, p);
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.DAYS.toMillis(2),
                millis_2017_02_28, p);

        // Freeze period with maintenance window
        p = SystemUpdatePolicy.createWindowedInstallPolicy(23 * 60, 1 * 60); // 23:00 - 1:00
        setFreezePeriods(p, "02-01", "02-28");
        // 00:00 is within the current window, outside freeze period
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.HOURS.toMillis(1),
                millis_2018_01_01, p);
        // Last day of feeze period, which ends in 22 hours
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.HOURS.toMillis(22),
                millis_2017_02_28 + TimeUnit.HOURS.toMillis(2), p);
        // Last day before the next freeze, and within window
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.HOURS.toMillis(1),
                millis_2017_01_31, p);
        // Last day before the next freeze, and there is still a partial maintenance window before
        // the freeze.
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_PAUSE, TimeUnit.HOURS.toMillis(19),
                millis_2017_01_31 + TimeUnit.HOURS.toMillis(4), p);

        // Two freeze periods
        p = SystemUpdatePolicy.createAutomaticInstallPolicy();
        setFreezePeriods(p, "05-01", "06-01", "10-15", "01-10");
        // automatic policy for July, August, September and October until 15th
        assertInstallationOption(
                SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC, TimeUnit.DAYS.toMillis(31 + 30 + 14),
                millis_2018_08_01, p);
    }

    private void assertInstallationOption(int expectedType, long expectedTime, long now,
            SystemUpdatePolicy p) {
        assertThat(p.getInstallationOptionAt(now).getType()).isEqualTo(expectedType);
        assertThat(p.getInstallationOptionAt(now).getEffectiveTime()).isEqualTo(expectedTime);
    }

    private void testFreezePeriodsSucceeds(String...dates) throws Exception {
        SystemUpdatePolicy p = SystemUpdatePolicy.createPostponeInstallPolicy();
        setFreezePeriods(p, dates);
    }

    private void testFreezePeriodsFails(int expectedError, String... dates) throws Exception {
        SystemUpdatePolicy p = SystemUpdatePolicy.createPostponeInstallPolicy();
        try {
            setFreezePeriods(p, dates);
            fail("Invalid periods (" + expectedError + ") not flagged: " + String.join(" ", dates));
        } catch (SystemUpdatePolicy.ValidationFailedException e) {
            assertWithMessage("Exception not expected: %s", e.getMessage()).that(e.getErrorCode())
                    .isEqualTo(expectedError);
        }
    }

    private void testPrevFreezePeriodSucceeds(String prevStart, String prevEnd, String now,
            String... dates) throws Exception {
        createPrevFreezePeriod(prevStart, prevEnd, now, dates);
    }

    private void testPrevFreezePeriodFails(int expectedError, String prevStart, String prevEnd,
            String now,  String... dates) throws Exception {
        try {
            createPrevFreezePeriod(prevStart, prevEnd, now, dates);
            fail("Invalid period (" + expectedError + ") not flagged: " + String.join(" ", dates));
        } catch (SystemUpdatePolicy.ValidationFailedException e) {
            assertWithMessage("Exception not expected: %s", e.getMessage()).that(e.getErrorCode())
                    .isEqualTo(expectedError);
        }
    }

    private void createPrevFreezePeriod(String prevStart, String prevEnd, String now,
            String... dates) throws Exception {
        SystemUpdatePolicy p = SystemUpdatePolicy.createPostponeInstallPolicy();
        setFreezePeriods(p, dates);
        p.validateAgainstPreviousFreezePeriod(parseLocalDate(prevStart),
                parseLocalDate(prevEnd), parseLocalDate(now));
    }

    // "MM-DD" format for date
    private void setFreezePeriods(SystemUpdatePolicy policy, String... dates) throws Exception {
        List<FreezePeriod> periods = new ArrayList<>();
        MonthDay lastDate = null;
        for (String date : dates) {
            MonthDay currentDate = parseMonthDay(date);
            if (lastDate != null) {
                periods.add(new FreezePeriod(lastDate, currentDate));
                lastDate = null;
            } else {
                lastDate = currentDate;
            }
        }
        policy.setFreezePeriods(periods);
        testSerialization(policy, periods);
    }

    private void testSerialization(SystemUpdatePolicy policy,
            List<FreezePeriod> expectedPeriods) throws Exception {
        // Test parcel / unparcel
        Parcel parcel = Parcel.obtain();
        policy.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        SystemUpdatePolicy q = SystemUpdatePolicy.CREATOR.createFromParcel(parcel);
        checkFreezePeriods(q, expectedPeriods);
        parcel.recycle();

        // Test XML serialization
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        final TypedXmlSerializer outXml = Xml.newFastSerializer();
        outXml.setOutput(outStream, StandardCharsets.UTF_8.name());
        outXml.startDocument(null, true);
        outXml.startTag(null, "ota");
        policy.saveToXml(outXml);
        outXml.endTag(null, "ota");
        outXml.endDocument();
        outXml.flush();

        ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray());
        TypedXmlPullParser parser = Xml.newFastPullParser();
        parser.setInput(new InputStreamReader(inStream));
        assertThat(parser.next()).isEqualTo(XmlPullParser.START_TAG);
        checkFreezePeriods(SystemUpdatePolicy.restoreFromXml(parser), expectedPeriods);
    }

    private void checkFreezePeriods(SystemUpdatePolicy policy,
            List<FreezePeriod> expectedPeriods) {
        int i = 0;
        for (FreezePeriod period : policy.getFreezePeriods()) {
            assertThat(period.getStart()).isEqualTo(expectedPeriods.get(i).getStart());
            assertThat(period.getEnd()).isEqualTo(expectedPeriods.get(i).getEnd());
            i++;
        }
    }

    // MonthDay is of format MM-dd
    private MonthDay parseMonthDay(String date) {
        return MonthDay.of(Integer.parseInt(date.substring(0, 2)),
                Integer.parseInt(date.substring(3, 5)));
    }

    // LocalDat is of format YYYY-MM-dd
    private LocalDate parseLocalDate(String date) {
        return parseMonthDay(date.substring(5)).atYear(Integer.parseInt(date.substring(0, 4)));
    }


    private long toMillis(int year, int month, int day) {
        return LocalDateTime.of(year, month, day, 0, 0, 0).atZone(ZoneId.systemDefault())
                .toInstant().toEpochMilli();
    }
}
