chromium/chromeos/ash/components/report/utils/time_utils_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/report/utils/time_utils.h"

#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "chromeos/ash/components/system/fake_statistics_provider.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/icu/source/i18n/unicode/timezone.h"

namespace ash::report::utils {

TEST(TimeUtilsTest, ConvertGmtToPt) {
  // Test case for converting to PT.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 1, 0, 28, 0, 0, 0}, &ts));

  // Set test clock to fake ts.
  base::SimpleTestClock test_clock;
  test_clock.SetNow(ts);

  base::Time pt_ts = ConvertGmtToPt(&test_clock);

  // Validate that the converted time is 8 hours behind GMT time.
  base::TimeDelta expected_diff = base::Hours(-8);
  EXPECT_EQ(pt_ts - ts, expected_diff);
}

TEST(TimeUtilsTest, ConvertGmtToPtDaylightSavings) {
  // Test case for converting to PT during daylight savings.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 3, 0, 13, 0, 0, 0}, &ts));

  // Set test clock to fake ts.
  base::SimpleTestClock test_clock;
  test_clock.SetNow(ts);

  base::Time pt_ts = ConvertGmtToPt(&test_clock);

  // Validate that the converted time is 7 hours behind GMT time.
  base::TimeDelta expected_diff = base::Hours(-7);
  EXPECT_EQ(pt_ts - ts, expected_diff);
}

TEST(TimeUtilsTest, ConvertGmtToPtUnknownTimeZone) {
  // Test case for converting to PT when the time zone is unknown.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 1, 0, 28, 0, 0, 0}, &ts));

  // Set test clock to fake ts.
  base::SimpleTestClock test_clock;
  test_clock.SetNow(ts);

  // Mock TimeZone::createTimeZone() to return unknown time zone
  icu::TimeZone::adoptDefault(
      icu::TimeZone::createTimeZone(icu::UnicodeString("UnknownTimeZone")));

  base::Time pt_ts = ConvertGmtToPt(&test_clock);

  // Validate that the converted time is 8 hours behind GMT time.
  base::TimeDelta expected_diff = base::Hours(-8);
  EXPECT_EQ(pt_ts - ts, expected_diff);
}

TEST(TimeUtilsTest, GetPreviousMonth) {
  // Test case for getting the previous month.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 5, 0, 28, 0, 0, 0}, &ts));
  base::Time previous_month_ts = GetPreviousMonth(ts).value();

  // Validate that the previous month is April 2023
  base::Time expected_previous_month_ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 4, 0, 1, 0, 0, 0},
                                          &expected_previous_month_ts));
  EXPECT_EQ(previous_month_ts, expected_previous_month_ts);
}

TEST(TimeUtilsTest, GetNextMonth) {
  // Test case for getting the next month.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 5, 0, 28, 0, 0, 0}, &ts));
  base::Time next_month_ts = GetNextMonth(ts).value();

  // Validate that the next month is June 2023
  base::Time expected_next_month_ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 6, 0, 1, 0, 0, 0},
                                          &expected_next_month_ts));
  EXPECT_EQ(next_month_ts, expected_next_month_ts);
}

TEST(TimeUtilsTest, GetPreviousYear) {
  // Test case for getting the previous year.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 5, 0, 28, 0, 0, 0}, &ts));
  base::Time previous_year_ts = GetPreviousYear(ts).value();

  // Validate that the previous year is 2022
  base::Time expected_previous_year_ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2022, 5, 0, 1, 0, 0, 0},
                                          &expected_previous_year_ts));
  EXPECT_EQ(previous_year_ts, expected_previous_year_ts);
}

TEST(TimeUtilsTest, GetPreviousMonthJanuary) {
  // Test case for getting the previous month when the current month is January.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 1, 0, 28, 0, 0, 0}, &ts));
  base::Time previous_month_ts = GetPreviousMonth(ts).value();

  // Validate that the previous month is December 2022
  base::Time expected_previous_month_ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2022, 12, 0, 1, 0, 0, 0},
                                          &expected_previous_month_ts));
  EXPECT_EQ(previous_month_ts, expected_previous_month_ts);
}

TEST(TimeUtilsTest, GetNextMonthDecember) {
  // Test case for getting the next month when the current month is December.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 12, 0, 28, 0, 0, 0}, &ts));
  base::Time next_month_ts = GetNextMonth(ts).value();

  // Validate that the next month is January 2024
  base::Time expected_next_month_ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2024, 1, 0, 1, 0, 0, 0},
                                          &expected_next_month_ts));
  EXPECT_EQ(next_month_ts, expected_next_month_ts);
}

TEST(TimeUtilsTest, GetPreviousYearJanuary) {
  // Test case for getting the previous year when the current year is January.
  base::Time ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 1, 0, 28, 0, 0, 0}, &ts));
  base::Time previous_year_ts = GetPreviousYear(ts).value();

  // Validate that the previous year is 2022
  base::Time expected_previous_year_ts;
  EXPECT_TRUE(base::Time::FromUTCExploded({2022, 1, 0, 1, 0, 0, 0},
                                          &expected_previous_year_ts));
  EXPECT_EQ(previous_year_ts, expected_previous_year_ts);
}

TEST(TimeUtilsTest, IsSameYearAndMonth) {
  // Test case for checking if two timestamps have the same year and month.
  base::Time ts1;
  base::Time ts2;
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 5, 0, 25, 0, 0, 0}, &ts1));
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 5, 0, 10, 0, 0, 0}, &ts2));

  // Validate that ts1 and ts2 have the same year and month
  EXPECT_TRUE(IsSameYearAndMonth(ts1, ts2));

  // Modify ts2 to have a different month
  EXPECT_TRUE(base::Time::FromUTCExploded({2023, 6, 0, 10, 0, 0, 0}, &ts2));
  EXPECT_FALSE(IsSameYearAndMonth(ts1, ts2));

  // Modify ts2 to have a different year
  EXPECT_TRUE(base::Time::FromUTCExploded({2024, 5, 0, 25, 0, 0, 0}, &ts2));
  EXPECT_FALSE(IsSameYearAndMonth(ts1, ts2));
}

TEST(TimeUtilsTest, FormatTimestampToMidnightGMTString) {
  // Test case for formatting a timestamp to midnight GMT string.
  base::Time ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2023, 5, 0, 25, 23, 59, 59, 999}, &ts));
  std::string formatted_ts = FormatTimestampToMidnightGMTString(ts);

  // Validate the formatted string
  std::string expected_formatted_ts = "2023-05-25 00:00:00.000 GMT";
  EXPECT_EQ(formatted_ts, expected_formatted_ts);
}

TEST(TimeUtilsTest, TimeToYYYYMMDDString) {
  // Test case for converting a time to YYYYMMDD string.
  base::Time ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2023, 5, 0, 25, 23, 59, 59, 999}, &ts));
  std::string formatted_ts = TimeToYYYYMMDDString(ts);

  // Validate the formatted string
  std::string expected_formatted_ts = "20230525";
  EXPECT_EQ(formatted_ts, expected_formatted_ts);
}

TEST(TimeUtilsTest, TimeToYYYYMMString) {
  // Test case for converting a time to YYYYMM string.
  base::Time ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2023, 5, 0, 25, 23, 59, 59, 999}, &ts));
  std::string formatted_ts = TimeToYYYYMMString(ts);

  // Validate the formatted string
  std::string expected_formatted_ts = "202305";
  EXPECT_EQ(formatted_ts, expected_formatted_ts);
}

TEST(TimeUtilsTest, GetFirstActiveWeek) {
  system::FakeStatisticsProvider statistics_provider;
  system::StatisticsProvider::SetTestProvider(&statistics_provider);

  // Mocking the StatisticsProvider for testing.
  statistics_provider.SetMachineStatistic(system::kActivateDateKey, "2023-18");

  // Call the method under test
  auto result = GetFirstActiveWeek();
  EXPECT_TRUE(result.has_value());

  // Calculate the expected result for comparison.
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2023, 5, 0, 1, 0, 0, 0, 0}, &expected_ts));

  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, FirstMondayOfISONewYear2000) {
  // Call the method under test.
  std::optional<base::Time> result = FirstMondayOfISONewYear(2000);

  // Calculate the expected result for comparison.
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2000, 1, 0, 3, 0, 0, 0, 0}, &expected_ts));

  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, FirstMondayOfISONewYear2001) {
  // Call the method under test.
  std::optional<base::Time> result = FirstMondayOfISONewYear(2001);

  // Calculate the expected result for comparison.
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2001, 1, 0, 1, 0, 0, 0, 0}, &expected_ts));

  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, FirstMondayOfISONewYear2020) {
  // Call the method under test.
  std::optional<base::Time> result = FirstMondayOfISONewYear(2020);

  // Calculate the expected result for comparison.
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2019, 12, 1, 30, 0, 0, 0, 0}, &expected_ts));

  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, FirstMondayOfISONewYear2025) {
  // Call the method under test.
  std::optional<base::Time> result = FirstMondayOfISONewYear(2025);

  // Calculate the expected result for comparison.
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2024, 12, 1, 30, 0, 0, 0, 0}, &expected_ts));

  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, Iso8601DateWeekAsTime_2023_W18) {
  // Call method under test and calculate the expected result for comparison.
  auto result = Iso8601DateWeekAsTime(2023, 18);
  EXPECT_TRUE(result.has_value());
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2023, 5, 0, 1, 0, 0, 0, 0}, &expected_ts));
  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, Iso8601DateWeekAsTime_2026_W01) {
  // Call method under test and calculate the expected result for comparison.
  auto result = Iso8601DateWeekAsTime(2026, 1);
  EXPECT_TRUE(result.has_value());
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2025, 12, 1, 29, 0, 0, 0, 0}, &expected_ts));
  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, Iso8601DateWeekAsTime_2026_W53) {
  // Call method under test and calculate the expected result for comparison.
  auto result = Iso8601DateWeekAsTime(2026, 53);
  EXPECT_TRUE(result.has_value());
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2026, 12, 1, 28, 0, 0, 0, 0}, &expected_ts));
  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, InvalidIso8601DateWeekAsTime) {
  // Call the method under test
  auto result = Iso8601DateWeekAsTime(0, 0);
  EXPECT_FALSE(result.has_value());

  result = Iso8601DateWeekAsTime(-1, 0);
  EXPECT_FALSE(result.has_value());

  // Week of year is greater than 53.
  result = Iso8601DateWeekAsTime(2023, 54);
  EXPECT_FALSE(result.has_value());
}

TEST(TimeUtilsTest, Iso8601DateWeekAsTimeWeek53) {
  // Call the method under test
  // I.e Year 2020 has 53 weeks instead of 52.
  auto result = Iso8601DateWeekAsTime(2020, 53);
  EXPECT_TRUE(result.has_value());

  // Calculate the expected result for comparison.
  base::Time expected_ts;
  EXPECT_TRUE(
      base::Time::FromUTCExploded({2020, 12, 0, 28, 0, 0, 0, 0}, &expected_ts));

  EXPECT_EQ(result.value(), expected_ts);
}

TEST(TimeUtilsTest, ConvertTimeToISO8601String) {
  // Call the method under test.
  // Cross validated against calculator: https://planetcalc.com/8540/
  struct {
    base::Time::Exploded exploded;
    std::string_view expected_yyyy_mm;
  } kTestCases[] = {
      {{2025, 1, 0, 1, 12, 30, 0, 0}, "2025-01"},
      {{2020, 1, 0, 1, 12, 30, 0, 0}, "2020-01"},
      {{2023, 1, 0, 1, 12, 30, 0, 0}, "2022-52"},
      {{2023, 12, 0, 30, 12, 30, 0, 0}, "2023-52"},
      {{2019, 12, 0, 30, 12, 30, 0, 0}, "2020-01"},
      {{2019, 8, 0, 25, 12, 30, 0, 0}, "2019-34"},
      {{2020, 8, 0, 25, 12, 30, 0, 0}, "2020-35"},
      {{2021, 8, 0, 25, 12, 30, 0, 0}, "2021-34"},
      {{2023, 11, 0, 25, 12, 30, 0, 0}, "2023-47"},
      {{2020, 12, 0, 27, 12, 30, 0, 0}, "2020-52"},
      {{2020, 12, 0, 28, 12, 30, 0, 0}, "2020-53"},
      {{2021, 1, 0, 4, 12, 30, 0, 0}, "2021-01"},
      // Edge Case: Returns 52 weeks, even though previous year has 53 weeks.
      // ISO calendar date to Gregorian calendar calculator outputs 2020-53.
      {{2021, 1, 0, 3, 12, 30, 0, 0}, "2020-52"},
  };

  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(testing::Message() << "Case: " << test_case.expected_yyyy_mm);
    base::Time test_ts;
    EXPECT_TRUE(base::Time::FromUTCExploded(test_case.exploded, &test_ts));
    EXPECT_EQ(ConvertTimeToISO8601String(test_ts), test_case.expected_yyyy_mm);
  }
}

TEST(TimeUtilsTest, IsFirstActiveUnderFourMonthsAgo) {
  struct {
    base::Time::Exploded active_exploded;
    base::Time::Exploded first_active_exploded;
    bool expected_result;
  } kTestCases[] = {
      // Check when active and first active represent the same time.
      {{2021, 1, 0, 3, 12, 30, 0, 0}, {2021, 1, 0, 3, 12, 30, 0, 0}, true},
      // Check boundary case if first active under four months ago.
      {{2021, 1, 0, 31, 12, 30, 0, 0}, {2020, 10, 0, 1, 12, 30, 0, 0}, true},
      // Check outside four month boundary is false.
      {{2021, 1, 0, 31, 12, 30, 0, 0}, {2020, 9, 0, 1, 12, 30, 0, 0}, false},
  };

  for (const auto& test_case : kTestCases) {
    base::Time active_ts;
    base::Time first_active_ts;
    EXPECT_TRUE(
        base::Time::FromUTCExploded(test_case.active_exploded, &active_ts));
    EXPECT_TRUE(base::Time::FromUTCExploded(test_case.first_active_exploded,
                                            &first_active_ts));

    // 4 Months ago assuming max 31 days per month.
    int max_days_in_4_months = 31 * 4;
    EXPECT_EQ(IsFirstActiveUnderNDaysAgo(active_ts, first_active_ts,
                                         max_days_in_4_months),
              test_case.expected_result);
  }
}

TEST(TimeUtilsTest, IsFirstActiveUnderFiveWeeksAgo) {
  struct {
    base::Time::Exploded active_exploded;
    base::Time::Exploded first_active_exploded;
    bool expected_result;
  } kTestCases[] = {
      // Check when active and first active represent the same time.
      {{2021, 1, 0, 3, 12, 30, 0, 0}, {2021, 1, 0, 3, 12, 30, 0, 0}, true},
      // Check boundary case if first active under 5 weeks ago.
      {{2021, 2, 0, 4, 12, 30, 0, 0}, {2020, 12, 0, 31, 12, 30, 0, 0}, true},
      // Check outside four month boundary is false.
      {{2021, 1, 0, 31, 12, 30, 0, 0}, {2020, 9, 0, 1, 12, 30, 0, 0}, false},
  };

  for (const auto& test_case : kTestCases) {
    base::Time active_ts;
    base::Time first_active_ts;
    EXPECT_TRUE(
        base::Time::FromUTCExploded(test_case.active_exploded, &active_ts));
    EXPECT_TRUE(base::Time::FromUTCExploded(test_case.first_active_exploded,
                                            &first_active_ts));

    int max_days_in_5_weeks = 7 * 5;
    EXPECT_EQ(IsFirstActiveUnderNDaysAgo(active_ts, first_active_ts,
                                         max_days_in_5_weeks),
              test_case.expected_result);
  }
}

}  // namespace ash::report::utils