// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/feed/core/v2/scheduling.h"

#include "base/check.h"
#include "base/json/json_writer.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "components/feed/core/v2/config.h"
#include "components/feed/core/v2/feedstore_util.h"
#include "components/feed/core/v2/public/stream_type.h"
#include "components/feed/feed_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace feed {
namespace {

const base::Time kAnchorTime = base::Time::UnixEpoch() + base::Hours(6);
const base::TimeDelta kDefaultScheduleInterval = base::Hours(24);

std::string ToJSON(base::ValueView value) {
  std::string json;
  CHECK(base::JSONWriter::WriteWithOptions(
      value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json));
  // Don't use \r\n on windows.
  base::RemoveChars(json, "\r", &json);
  return json;
}

TEST(RequestSchedule, CanSerialize) {
  RequestSchedule schedule;
  schedule.anchor_time = kAnchorTime;
  schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
  schedule.type = RequestSchedule::Type::kScheduledRefresh;

  const base::DictValue schedule_dict = RequestScheduleToDict(schedule);
  ASSERT_EQ(R"({
   "anchor": "11644495200000000",
   "offsets": [ "3600000000", "21600000000" ],
   "type": 0
}
)",
            ToJSON(schedule_dict));

  RequestSchedule deserialized_schedule =
      RequestScheduleFromDict(schedule_dict);
  EXPECT_EQ(schedule.anchor_time, deserialized_schedule.anchor_time);
  EXPECT_EQ(schedule.refresh_offsets, deserialized_schedule.refresh_offsets);
  EXPECT_EQ(schedule.type, deserialized_schedule.type);
}

TEST(RequestSchedule, GetScheduleType) {
  RequestSchedule schedule;
  schedule.anchor_time = kAnchorTime;
  schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
  schedule.type = RequestSchedule::Type::kScheduledRefresh;
  EXPECT_EQ(RequestSchedule::Type::kScheduledRefresh,
            RequestScheduleFromDict(RequestScheduleToDict(schedule)).type);
  schedule.type = RequestSchedule::Type::kFeedCloseRefresh;
  base::DictValue schedule_dict = RequestScheduleToDict(schedule);
  EXPECT_EQ(RequestSchedule::Type::kFeedCloseRefresh,
            RequestScheduleFromDict(schedule_dict).type);
  // Default to kScheduledRefresh if the type isn't valid.
  schedule_dict.Set("type", -1);
  EXPECT_EQ(RequestSchedule::Type::kScheduledRefresh,
            RequestScheduleFromDict(schedule_dict).type);
}

class NextScheduledRequestTimeTest : public testing::Test {
 public:
  void SetUp() override {
    Config config = GetFeedConfig();
    config.default_background_refresh_interval = kDefaultScheduleInterval;
    SetFeedConfigForTesting(config);
  }
};

TEST_F(NextScheduledRequestTimeTest, NormalUsage) {
  RequestSchedule schedule;
  schedule.anchor_time = kAnchorTime;
  schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};

  // |kNow| is in the normal range [kAnchorTime, kAnchorTime+1hr)
  base::Time kNow = kAnchorTime + base::Minutes(12);
  EXPECT_EQ(kAnchorTime + base::Hours(1),
            NextScheduledRequestTime(kNow, &schedule));
  kNow += base::Hours(1);
  EXPECT_EQ(kAnchorTime + base::Hours(6),
            NextScheduledRequestTime(kNow, &schedule));
  kNow += base::Hours(6);
  EXPECT_EQ(kNow + kDefaultScheduleInterval,
            NextScheduledRequestTime(kNow, &schedule));
}

TEST_F(NextScheduledRequestTimeTest, NowPastRequestTimeSkipsRequest) {
  RequestSchedule schedule;
  schedule.anchor_time = kAnchorTime;
  schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};

  base::Time kNow = kAnchorTime + base::Minutes(61);
  EXPECT_EQ(kAnchorTime + base::Hours(6),
            NextScheduledRequestTime(kNow, &schedule));
  kNow += base::Hours(6);
  EXPECT_EQ(kNow + kDefaultScheduleInterval,
            NextScheduledRequestTime(kNow, &schedule));
}

TEST_F(NextScheduledRequestTimeTest, NowPastAllRequestTimes) {
  RequestSchedule schedule;
  schedule.anchor_time = kAnchorTime;
  schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};

  base::Time kNow = kAnchorTime + base::Hours(7);
  EXPECT_EQ(kNow + kDefaultScheduleInterval,
            NextScheduledRequestTime(kNow, &schedule));
}

TEST_F(NextScheduledRequestTimeTest, NowInPast) {
  RequestSchedule schedule;
  schedule.anchor_time = kAnchorTime;
  schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};

  // Since |kNow| is in the past, deltas are recomputed using |kNow|.
  base::Time kNow = kAnchorTime - base::Minutes(12);
  EXPECT_EQ(kNow + base::Hours(1), NextScheduledRequestTime(kNow, &schedule));
  EXPECT_EQ(kNow, schedule.anchor_time);
}

TEST_F(NextScheduledRequestTimeTest, NowInFarFuture) {
  RequestSchedule schedule;
  schedule.anchor_time = kAnchorTime;
  schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};

  // Since |kNow| is in the far future, deltas are recomputed using |kNow|.
  base::Time kNow = kAnchorTime + base::Days(12);
  EXPECT_EQ(kNow + base::Hours(1), NextScheduledRequestTime(kNow, &schedule));
  EXPECT_EQ(kNow, schedule.anchor_time);
}

class ContentLifetimeTest : public testing::Test {
 public:
  const base::TimeDelta kDefaultContentExpiration = base::Hours(24);
  const base::TimeDelta kDefaultStaleContentThreshold = base::Hours(4);
  const base::TimeDelta kDefaultSubscriptionlessContentExpiration =
      base::Days(14);

  void SetUp() override {
    Config config = GetFeedConfig();
    config.content_expiration_threshold = kDefaultContentExpiration;
    config.stale_content_threshold = kDefaultStaleContentThreshold;
    SetFeedConfigForTesting(config);

    metadata_ = feedstore::Metadata();
    feedstore::Metadata::StreamMetadata* sm = metadata_.add_stream_metadata();
    sm->set_stream_key(
        std::string(feedstore::StreamKey(StreamType(StreamKind::kForYou))));
  }

 protected:
  void set_content_lifetime(int64_t stale_age_ms, int64_t invalid_age_ms) {
    feedstore::Metadata::StreamMetadata::ContentLifetime* content_lifetime =
        MetadataForStream(metadata_, StreamType(StreamKind::kForYou))
            .mutable_content_lifetime();
    content_lifetime->set_stale_age_ms(stale_age_ms);
    content_lifetime->set_invalid_age_ms(invalid_age_ms);
  }

  void set_stale_age(base::TimeDelta stale_age) {
    set_content_lifetime(stale_age.InMilliseconds(), 0);
  }

  void set_invalid_age(base::TimeDelta invalid_age) {
    set_content_lifetime(0, invalid_age.InMilliseconds());
  }

  base::TimeDelta WithEpsilon(base::TimeDelta duration) {
    return duration + base::Milliseconds(1);
  }

  feedstore::Metadata metadata_;
};

TEST_F(ContentLifetimeTest, ShouldWaitForNewContent_DefaultThreshold) {
  EXPECT_FALSE(ShouldWaitForNewContent(metadata_,
                                       StreamType(StreamKind::kForYou),
                                       kDefaultStaleContentThreshold));
  EXPECT_TRUE(
      ShouldWaitForNewContent(metadata_, StreamType(StreamKind::kForYou),
                              WithEpsilon(kDefaultStaleContentThreshold)));
  EXPECT_TRUE(ShouldWaitForNewContent(
      metadata_, StreamType(StreamKind::kForYou), base::Hours(5)));
  EXPECT_FALSE(ShouldWaitForNewContent(
      metadata_, StreamType(StreamKind::kForYou), base::Hours(3)));
  // If the web feed onboarding feature is turned off, then we should return
  // true even if the user is not subscribed.
  EXPECT_TRUE(ShouldWaitForNewContent(
      metadata_, StreamType(StreamKind::kForYou), base::Days(8)));
  EXPECT_TRUE(ShouldWaitForNewContent(
      metadata_, StreamType(StreamKind::kForYou), base::Days(6)));
}

TEST_F(ContentLifetimeTest, ShouldWaitForNewContent_ServerThreshold_Valid) {
  set_stale_age(base::Minutes(60));
  EXPECT_TRUE(ShouldWaitForNewContent(
      metadata_, StreamType(StreamKind::kForYou), base::Minutes(61)));
  EXPECT_FALSE(ShouldWaitForNewContent(
      metadata_, StreamType(StreamKind::kForYou), base::Minutes(59)));
}

TEST_F(ContentLifetimeTest, ShouldWaitForNewContent_ServerThreshold_Invalid) {
  // We ignore stale ages greater than the default.
  EXPECT_TRUE(
      ShouldWaitForNewContent(metadata_, StreamType(StreamKind::kForYou),
                              WithEpsilon(kDefaultStaleContentThreshold)));
  set_stale_age(kDefaultStaleContentThreshold + base::Minutes(1));
  EXPECT_TRUE(
      ShouldWaitForNewContent(metadata_, StreamType(StreamKind::kForYou),
                              WithEpsilon(kDefaultStaleContentThreshold)));

  // We ignore zero durations.
  set_stale_age(base::Days(0));
  EXPECT_FALSE(ShouldWaitForNewContent(metadata_,
                                       StreamType(StreamKind::kForYou),
                                       kDefaultStaleContentThreshold));
  EXPECT_TRUE(
      ShouldWaitForNewContent(metadata_, StreamType(StreamKind::kForYou),
                              WithEpsilon(kDefaultStaleContentThreshold)));

  // We ignore negative durations.
  set_stale_age(base::Days(-1));
  EXPECT_FALSE(ShouldWaitForNewContent(metadata_,
                                       StreamType(StreamKind::kForYou),
                                       kDefaultStaleContentThreshold));
  EXPECT_TRUE(
      ShouldWaitForNewContent(metadata_, StreamType(StreamKind::kForYou),
                              WithEpsilon(kDefaultStaleContentThreshold)));
}

TEST_F(ContentLifetimeTest, ContentInvalidFromAge_DefaultThreshold) {
  EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                     kDefaultContentExpiration));
  EXPECT_TRUE(
      ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                            kDefaultContentExpiration + base::Milliseconds(1)));
  EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                    base::Hours(25)));
  EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                     base::Hours(23)));
}

TEST_F(ContentLifetimeTest, ContentInvalidFromAge_ServerThreshold_Valid) {
  set_invalid_age(base::Minutes(60));
  EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                    base::Minutes(61)));
  EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                     base::Minutes(59)));
}

TEST_F(ContentLifetimeTest, ContentInvalidFromAge_ServerThreshold_Invalid) {
  // We ignore stale ages greater than the default.
  EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                    WithEpsilon(kDefaultContentExpiration)));
  set_invalid_age(kDefaultContentExpiration + base::Minutes(1));
  EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                    WithEpsilon(kDefaultContentExpiration)));

  // We ignore zero durations.
  set_invalid_age(base::Days(0));
  EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                     kDefaultContentExpiration));
  EXPECT_TRUE(ContentInvalidFromAge(
      metadata_, StreamType(StreamKind::kForYou),
      WithEpsilon(kDefaultSubscriptionlessContentExpiration)));

  // We ignore negative durations.
  set_invalid_age(base::Days(-1));
  EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                     kDefaultContentExpiration));
  EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
                                    WithEpsilon(kDefaultContentExpiration)));
}

}  // namespace
}  // namespace feed
