// Copyright (c) 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "quiche/quic/core/quic_ping_manager.h"

#include "quiche/quic/core/quic_one_block_arena.h"
#include "quiche/quic/platform/api/quic_test.h"
#include "quiche/quic/test_tools/quic_test_utils.h"

namespace quic {
namespace test {

class QuicPingManagerPeer {
 public:
  static QuicAlarm* GetAlarm(QuicPingManager* manager) {
    return manager->alarm_.get();
  }

  static void SetPerspective(QuicPingManager* manager,
                             Perspective perspective) {
    manager->perspective_ = perspective;
  }
};

namespace {

const bool kShouldKeepAlive = true;
const bool kHasInflightPackets = true;

class MockDelegate : public QuicPingManager::Delegate {
 public:
  MOCK_METHOD(void, OnKeepAliveTimeout, (), (override));
  MOCK_METHOD(void, OnRetransmittableOnWireTimeout, (), (override));
};

class QuicPingManagerTest : public QuicTest {
 public:
  QuicPingManagerTest()
      : manager_(Perspective::IS_CLIENT, &delegate_, &arena_, &alarm_factory_,
                 /*context=*/nullptr),
        alarm_(static_cast<MockAlarmFactory::TestAlarm*>(
            QuicPingManagerPeer::GetAlarm(&manager_))) {
    clock_.AdvanceTime(QuicTime::Delta::FromSeconds(1));
  }

 protected:
  testing::StrictMock<MockDelegate> delegate_;
  MockClock clock_;
  QuicConnectionArena arena_;
  MockAlarmFactory alarm_factory_;
  QuicPingManager manager_;
  MockAlarmFactory::TestAlarm* alarm_;
};

TEST_F(QuicPingManagerTest, KeepAliveTimeout) {
  EXPECT_FALSE(alarm_->IsSet());

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  // Set alarm with in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  // Reset alarm with no in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  // Verify the deadline is set slightly less than 15 seconds in the future,
  // because of the 1s alarm granularity.
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs) -
                QuicTime::Delta::FromMilliseconds(5),
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(QuicTime::Delta::FromSeconds(kPingTimeoutSecs));
  EXPECT_CALL(delegate_, OnKeepAliveTimeout());
  alarm_->Fire();
  EXPECT_FALSE(alarm_->IsSet());
  // Reset alarm with in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());

  // Verify alarm is not armed if !kShouldKeepAlive.
  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  manager_.SetAlarm(clock_.ApproximateNow(), !kShouldKeepAlive,
                    kHasInflightPackets);
  EXPECT_FALSE(alarm_->IsSet());
}

TEST_F(QuicPingManagerTest, CustomizedKeepAliveTimeout) {
  EXPECT_FALSE(alarm_->IsSet());

  // Set customized keep-alive timeout.
  manager_.set_keep_alive_timeout(QuicTime::Delta::FromSeconds(10));

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  // Set alarm with in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(10),
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  // Set alarm with no in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  // The deadline is set slightly less than 10 seconds in the future, because
  // of the 1s alarm granularity.
  EXPECT_EQ(
      QuicTime::Delta::FromSeconds(10) - QuicTime::Delta::FromMilliseconds(5),
      alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(QuicTime::Delta::FromSeconds(10));
  EXPECT_CALL(delegate_, OnKeepAliveTimeout());
  alarm_->Fire();
  EXPECT_FALSE(alarm_->IsSet());
  // Reset alarm with in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());

  // Verify alarm is not armed if !kShouldKeepAlive.
  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  manager_.SetAlarm(clock_.ApproximateNow(), !kShouldKeepAlive,
                    kHasInflightPackets);
  EXPECT_FALSE(alarm_->IsSet());
}

TEST_F(QuicPingManagerTest, RetransmittableOnWireTimeout) {
  const QuicTime::Delta kRtransmittableOnWireTimeout =
      QuicTime::Delta::FromMilliseconds(50);
  manager_.set_initial_retransmittable_on_wire_timeout(
      kRtransmittableOnWireTimeout);

  EXPECT_FALSE(alarm_->IsSet());

  // Set alarm with in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  // Verify alarm is in keep-alive mode.
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  // Set alarm with no in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  // Verify alarm is in retransmittable-on-wire mode.
  EXPECT_EQ(kRtransmittableOnWireTimeout,
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(kRtransmittableOnWireTimeout);
  EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
  alarm_->Fire();
  EXPECT_FALSE(alarm_->IsSet());
  // Reset alarm with in flight packets.
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  // Verify the alarm is in keep-alive mode.
  ASSERT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());
}

TEST_F(QuicPingManagerTest, RetransmittableOnWireTimeoutExponentiallyBackOff) {
  const int kMaxAggressiveRetransmittableOnWireCount = 5;
  SetQuicFlag(quic_max_aggressive_retransmittable_on_wire_ping_count,
              kMaxAggressiveRetransmittableOnWireCount);
  const QuicTime::Delta initial_retransmittable_on_wire_timeout =
      QuicTime::Delta::FromMilliseconds(200);
  manager_.set_initial_retransmittable_on_wire_timeout(
      initial_retransmittable_on_wire_timeout);

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  EXPECT_FALSE(alarm_->IsSet());
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  // Verify alarm is in keep-alive mode.
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());

  // Verify no exponential backoff on the first few retransmittable on wire
  // timeouts.
  for (int i = 0; i <= kMaxAggressiveRetransmittableOnWireCount; ++i) {
    clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
    // Reset alarm with no in flight packets.
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      !kHasInflightPackets);
    EXPECT_TRUE(alarm_->IsSet());
    // Verify alarm is in retransmittable-on-wire mode.
    EXPECT_EQ(initial_retransmittable_on_wire_timeout,
              alarm_->deadline() - clock_.ApproximateNow());
    clock_.AdvanceTime(initial_retransmittable_on_wire_timeout);
    EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
    alarm_->Fire();
    EXPECT_FALSE(alarm_->IsSet());
    // Reset alarm with in flight packets.
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      kHasInflightPackets);
  }

  QuicTime::Delta retransmittable_on_wire_timeout =
      initial_retransmittable_on_wire_timeout;

  // Verify subsequent retransmittable-on-wire timeout is exponentially backed
  // off.
  while (retransmittable_on_wire_timeout * 2 <
         QuicTime::Delta::FromSeconds(kPingTimeoutSecs)) {
    retransmittable_on_wire_timeout = retransmittable_on_wire_timeout * 2;
    clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      !kHasInflightPackets);
    EXPECT_TRUE(alarm_->IsSet());
    EXPECT_EQ(retransmittable_on_wire_timeout,
              alarm_->deadline() - clock_.ApproximateNow());

    clock_.AdvanceTime(retransmittable_on_wire_timeout);
    EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
    alarm_->Fire();
    EXPECT_FALSE(alarm_->IsSet());
    // Reset alarm with in flight packets.
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      kHasInflightPackets);
  }

  // Verify alarm is in keep-alive mode.
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  // Reset alarm with no in flight packets
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  // Verify alarm is in keep-alive mode because retransmittable-on-wire deadline
  // is later.
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs) -
                QuicTime::Delta::FromMilliseconds(5),
            alarm_->deadline() - clock_.ApproximateNow());
  clock_.AdvanceTime(QuicTime::Delta::FromSeconds(kPingTimeoutSecs) -
                     QuicTime::Delta::FromMilliseconds(5));
  EXPECT_CALL(delegate_, OnKeepAliveTimeout());
  alarm_->Fire();
  EXPECT_FALSE(alarm_->IsSet());
}

TEST_F(QuicPingManagerTest,
       ResetRetransmitableOnWireTimeoutExponentiallyBackOff) {
  const int kMaxAggressiveRetransmittableOnWireCount = 3;
  SetQuicFlag(quic_max_aggressive_retransmittable_on_wire_ping_count,
              kMaxAggressiveRetransmittableOnWireCount);
  const QuicTime::Delta initial_retransmittable_on_wire_timeout =
      QuicTime::Delta::FromMilliseconds(200);
  manager_.set_initial_retransmittable_on_wire_timeout(
      initial_retransmittable_on_wire_timeout);

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  EXPECT_FALSE(alarm_->IsSet());
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);
  // Verify alarm is in keep-alive mode.
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  // Verify alarm is in retransmittable-on-wire mode.
  EXPECT_EQ(initial_retransmittable_on_wire_timeout,
            alarm_->deadline() - clock_.ApproximateNow());

  EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
  clock_.AdvanceTime(initial_retransmittable_on_wire_timeout);
  alarm_->Fire();

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(initial_retransmittable_on_wire_timeout,
            alarm_->deadline() - clock_.ApproximateNow());

  manager_.reset_consecutive_retransmittable_on_wire_count();
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_EQ(initial_retransmittable_on_wire_timeout,
            alarm_->deadline() - clock_.ApproximateNow());
  EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
  clock_.AdvanceTime(initial_retransmittable_on_wire_timeout);
  alarm_->Fire();

  for (int i = 0; i < kMaxAggressiveRetransmittableOnWireCount; i++) {
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      !kHasInflightPackets);
    EXPECT_TRUE(alarm_->IsSet());
    EXPECT_EQ(initial_retransmittable_on_wire_timeout,
              alarm_->deadline() - clock_.ApproximateNow());
    clock_.AdvanceTime(initial_retransmittable_on_wire_timeout);
    EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
    alarm_->Fire();
    // Reset alarm with in flight packets.
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      kHasInflightPackets);
    // Advance 5ms to receive next packet.
    clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  }

  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(initial_retransmittable_on_wire_timeout * 2,
            alarm_->deadline() - clock_.ApproximateNow());

  clock_.AdvanceTime(2 * initial_retransmittable_on_wire_timeout);
  EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
  alarm_->Fire();

  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
  manager_.reset_consecutive_retransmittable_on_wire_count();
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(initial_retransmittable_on_wire_timeout,
            alarm_->deadline() - clock_.ApproximateNow());
}

TEST_F(QuicPingManagerTest, RetransmittableOnWireLimit) {
  static constexpr int kMaxRetransmittableOnWirePingCount = 3;
  SetQuicFlag(quic_max_retransmittable_on_wire_ping_count,
              kMaxRetransmittableOnWirePingCount);
  static constexpr QuicTime::Delta initial_retransmittable_on_wire_timeout =
      QuicTime::Delta::FromMilliseconds(200);
  static constexpr QuicTime::Delta kShortDelay =
      QuicTime::Delta::FromMilliseconds(5);
  ASSERT_LT(kShortDelay * 10, initial_retransmittable_on_wire_timeout);
  manager_.set_initial_retransmittable_on_wire_timeout(
      initial_retransmittable_on_wire_timeout);

  clock_.AdvanceTime(kShortDelay);
  EXPECT_FALSE(alarm_->IsSet());
  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    kHasInflightPackets);

  EXPECT_TRUE(alarm_->IsSet());
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());

  for (int i = 0; i <= kMaxRetransmittableOnWirePingCount; i++) {
    clock_.AdvanceTime(kShortDelay);
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      !kHasInflightPackets);
    EXPECT_TRUE(alarm_->IsSet());
    EXPECT_EQ(initial_retransmittable_on_wire_timeout,
              alarm_->deadline() - clock_.ApproximateNow());
    clock_.AdvanceTime(initial_retransmittable_on_wire_timeout);
    EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
    alarm_->Fire();
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      kHasInflightPackets);
  }

  manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                    !kHasInflightPackets);
  EXPECT_TRUE(alarm_->IsSet());
  // Verify alarm is in keep-alive mode.
  EXPECT_EQ(QuicTime::Delta::FromSeconds(kPingTimeoutSecs),
            alarm_->deadline() - clock_.ApproximateNow());
  clock_.AdvanceTime(QuicTime::Delta::FromSeconds(kPingTimeoutSecs));
  EXPECT_CALL(delegate_, OnKeepAliveTimeout());
  alarm_->Fire();
  EXPECT_FALSE(alarm_->IsSet());
}

TEST_F(QuicPingManagerTest, MaxRetransmittableOnWireDelayShift) {
  QuicPingManagerPeer::SetPerspective(&manager_, Perspective::IS_SERVER);
  const int kMaxAggressiveRetransmittableOnWireCount = 3;
  SetQuicFlag(quic_max_aggressive_retransmittable_on_wire_ping_count,
              kMaxAggressiveRetransmittableOnWireCount);
  const QuicTime::Delta initial_retransmittable_on_wire_timeout =
      QuicTime::Delta::FromMilliseconds(200);
  manager_.set_initial_retransmittable_on_wire_timeout(
      initial_retransmittable_on_wire_timeout);

  for (int i = 0; i <= kMaxAggressiveRetransmittableOnWireCount; i++) {
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      !kHasInflightPackets);
    EXPECT_TRUE(alarm_->IsSet());
    EXPECT_EQ(initial_retransmittable_on_wire_timeout,
              alarm_->deadline() - clock_.ApproximateNow());
    clock_.AdvanceTime(initial_retransmittable_on_wire_timeout);
    EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
    alarm_->Fire();
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      kHasInflightPackets);
  }
  for (int i = 1; i <= 20; ++i) {
    manager_.SetAlarm(clock_.ApproximateNow(), kShouldKeepAlive,
                      !kHasInflightPackets);
    EXPECT_TRUE(alarm_->IsSet());
    if (i <= 10) {
      EXPECT_EQ(initial_retransmittable_on_wire_timeout * (1 << i),
                alarm_->deadline() - clock_.ApproximateNow());
    } else {
      // Verify shift is capped.
      EXPECT_EQ(initial_retransmittable_on_wire_timeout * (1 << 10),
                alarm_->deadline() - clock_.ApproximateNow());
    }
    clock_.AdvanceTime(alarm_->deadline() - clock_.ApproximateNow());
    EXPECT_CALL(delegate_, OnRetransmittableOnWireTimeout());
    alarm_->Fire();
  }
}

}  // namespace

}  // namespace test
}  // namespace quic
