/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 */

#include <cstring>
#include <folly/net/TcpInfo.h>
#include <folly/net/test/MockNetOpsDispatcher.h>
#include <folly/net/test/TcpInfoTestUtil.h>
#include <folly/portability/GMock.h>
#include <folly/portability/GTest.h>

using namespace folly;
using namespace testing;
using namespace folly::test;

using us = std::chrono::microseconds;

const auto kTestSiocoutqVal = 10U;
const auto kTestSiocinqVal = 100U;
const auto kTestUnknownCcName = "coolNewCCA"; // it's cool, new, and a CCA!

// tests are only supported on Linux right now
#ifdef __linux__
#include <linux/sockios.h>
#include <sys/ioctl.h>

class TcpInfoTest : public Test {
 public:
  template <typename T1>
  void setupExpectCallTcpInfo(NetworkSocket& s, const T1& tInfo) {
    TcpInfoTestUtil::setupExpectCallTcpInfo(mockNetOpsDispatcher_, s, tInfo);
  }

  void setupExpectCallCcName(NetworkSocket& s, const std::string& ccName) {
    TcpInfoTestUtil::setupExpectCallCcName(mockNetOpsDispatcher_, s, ccName);
  }

  void setupExpectCallCcInfo(
      NetworkSocket& s, const folly::TcpInfo::tcp_cc_info& ccInfo) {
    TcpInfoTestUtil::setupExpectCallCcInfo(mockNetOpsDispatcher_, s, ccInfo);
  }

  struct ExpectCallMemInfoConfig {
    size_t siocoutq{0};
    size_t siocinq{0};
  };

  void setupExpectCallMemInfo(
      NetworkSocket& s, const ExpectCallMemInfoConfig& config) {
    EXPECT_CALL(mockIoctlDispatcher_, ioctl(s.toFd(), SIOCOUTQ, testing::_))
        .WillOnce(WithArgs<2>(Invoke([config](void* argp) {
          size_t* val = static_cast<size_t*>(argp);
          *val = config.siocoutq;
          return 0;
        })));
    EXPECT_CALL(mockIoctlDispatcher_, ioctl(s.toFd(), SIOCINQ, testing::_))
        .WillOnce(WithArgs<2>(Invoke([config](void* argp) {
          size_t* val = static_cast<size_t*>(argp);
          *val = config.siocinq;
          return 0;
        })));
  }

  static folly::TcpInfo::tcp_info getTestLatestTcpInfo() {
    folly::TcpInfo::tcp_info tInfo = {};
    tInfo.tcpi_state = 1;
    tInfo.tcpi_ca_state = 2;
    tInfo.tcpi_retransmits = 3;
    tInfo.tcpi_probes = 4;
    tInfo.tcpi_backoff = 5;
    tInfo.tcpi_options = 6;
    tInfo.tcpi_snd_wscale = 7;
    tInfo.tcpi_rcv_wscale = 8;
    tInfo.tcpi_delivery_rate_app_limited = 1;
    tInfo.tcpi_rto = 9;
    tInfo.tcpi_ato = 10;
    tInfo.tcpi_snd_mss = 11;
    tInfo.tcpi_rcv_mss = 12;
    tInfo.tcpi_unacked = 113; // 113 instead of 13 for packets in flight
    tInfo.tcpi_sacked = 14;
    tInfo.tcpi_lost = 15;
    tInfo.tcpi_retrans = 16;
    tInfo.tcpi_fackets = 17;
    tInfo.tcpi_last_data_sent = 18;
    tInfo.tcpi_last_ack_sent = 19;
    tInfo.tcpi_last_data_recv = 20;
    tInfo.tcpi_last_ack_recv = 21;
    tInfo.tcpi_pmtu = 22;
    tInfo.tcpi_rcv_ssthresh = 23;
    tInfo.tcpi_rtt = 24;
    tInfo.tcpi_rttvar = 25;
    tInfo.tcpi_snd_ssthresh = 26;
    tInfo.tcpi_snd_cwnd = 27;
    tInfo.tcpi_advmss = 28;
    tInfo.tcpi_reordering = 29;
    tInfo.tcpi_rcv_rtt = 30;
    tInfo.tcpi_rcv_space = 31;
    tInfo.tcpi_total_retrans = 32;
    tInfo.tcpi_pacing_rate = 33;
    tInfo.tcpi_max_pacing_rate = 34;
    tInfo.tcpi_bytes_acked = 35;
    tInfo.tcpi_bytes_received = 36;
    tInfo.tcpi_segs_out = 37;
    tInfo.tcpi_segs_in = 38;
    tInfo.tcpi_notsent_bytes = 39;
    tInfo.tcpi_min_rtt = 40;
    tInfo.tcpi_data_segs_in = 41;
    tInfo.tcpi_data_segs_out = 42;
    tInfo.tcpi_delivery_rate = 43;
    tInfo.tcpi_busy_time = 44;
    tInfo.tcpi_rwnd_limited = 45;
    tInfo.tcpi_sndbuf_limited = 46;
    tInfo.tcpi_delivered = 47;
    tInfo.tcpi_delivered_ce = 48;
    tInfo.tcpi_bytes_sent = 49;
    tInfo.tcpi_bytes_retrans = 50;
    tInfo.tcpi_dsack_dups = 51;
    tInfo.tcpi_reord_seen = 52;
    tInfo.tcpi_rcv_ooopack = 53;
    tInfo.tcpi_snd_wnd = 54;
    return tInfo;
  }

  static folly::detail::tcp_info_legacy getTestLegacyTcpInfo() {
    folly::detail::tcp_info_legacy tInfo = {};
    tInfo.tcpi_state = 1;
    tInfo.tcpi_ca_state = 2;
    tInfo.tcpi_retransmits = 3;
    tInfo.tcpi_probes = 4;
    tInfo.tcpi_backoff = 5;
    tInfo.tcpi_options = 6;
    tInfo.tcpi_snd_wscale = 7;
    tInfo.tcpi_rcv_wscale = 8;
    tInfo.tcpi_rto = 9;
    tInfo.tcpi_ato = 10;
    tInfo.tcpi_snd_mss = 11;
    tInfo.tcpi_rcv_mss = 12;
    tInfo.tcpi_unacked = 113; // 113 instead of 13 for packets in flight
    tInfo.tcpi_sacked = 14;
    tInfo.tcpi_lost = 15;
    tInfo.tcpi_retrans = 16;
    tInfo.tcpi_fackets = 17;
    tInfo.tcpi_last_data_sent = 18;
    tInfo.tcpi_last_ack_sent = 19;
    tInfo.tcpi_last_data_recv = 20;
    tInfo.tcpi_last_ack_recv = 21;
    tInfo.tcpi_pmtu = 22;
    tInfo.tcpi_rcv_ssthresh = 23;
    tInfo.tcpi_rtt = 24;
    tInfo.tcpi_rttvar = 25;
    tInfo.tcpi_snd_ssthresh = 26;
    tInfo.tcpi_snd_cwnd = 27;
    tInfo.tcpi_advmss = 28;
    tInfo.tcpi_reordering = 29;
    tInfo.tcpi_rcv_rtt = 30;
    tInfo.tcpi_rcv_space = 31;
    tInfo.tcpi_total_retrans = 32;
    return tInfo;
  }

  static folly::TcpInfo::tcp_cc_info getTestBbrInfo() {
    folly::TcpInfo::tcp_cc_info ccInfo = {};
    ccInfo.bbr.bbr_bw_lo = 1;
    ccInfo.bbr.bbr_bw_hi = 2;
    ccInfo.bbr.bbr_min_rtt = 3;
    ccInfo.bbr.bbr_pacing_gain = 4;
    ccInfo.bbr.bbr_cwnd_gain = 5;
    return ccInfo;
  }

  static folly::TcpInfo::tcp_cc_info getTestVegasInfo() {
    folly::TcpInfo::tcp_cc_info ccInfo = {};
    ccInfo.vegas.tcpv_enabled = 6;
    ccInfo.vegas.tcpv_rttcnt = 7;
    ccInfo.vegas.tcpv_rtt = 8;
    ccInfo.vegas.tcpv_minrtt = 9;
    return ccInfo;
  }

  static folly::TcpInfo::tcp_cc_info getTestDctcpInfo() {
    folly::TcpInfo::tcp_cc_info ccInfo = {};
    ccInfo.dctcp.dctcp_enabled = 10;
    ccInfo.dctcp.dctcp_ce_state = 11;
    ccInfo.dctcp.dctcp_alpha = 12;
    ccInfo.dctcp.dctcp_ab_ecn = 13;
    ccInfo.dctcp.dctcp_ab_tot = 14;
    return ccInfo;
  }

  static void checkNoTcpInfo(const TcpInfo& wrappedTcpInfo) {
    // check a few accessors to make sure nothing available
    EXPECT_FALSE(wrappedTcpInfo.bytesRetransmitted().has_value());
    EXPECT_FALSE(wrappedTcpInfo.packetsRetransmitted().has_value());
    EXPECT_FALSE(wrappedTcpInfo.srtt().has_value());
    EXPECT_FALSE(wrappedTcpInfo.cwndInBytes().has_value());
    EXPECT_FALSE(wrappedTcpInfo.cwndInPackets().has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_info::tcpi_total_retrans)
            .has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_info::tcpi_snd_cwnd)
            .has_value());
  }

  static void checkTcpInfoAgainstLegacy(const TcpInfo& wrappedTcpInfo) {
    const auto& controlTcpInfo = getTestLegacyTcpInfo();

    EXPECT_FALSE(wrappedTcpInfo.minrtt());
    EXPECT_EQ(us(controlTcpInfo.tcpi_rtt), wrappedTcpInfo.srtt());

    EXPECT_FALSE(wrappedTcpInfo.bytesSent());
    EXPECT_FALSE(wrappedTcpInfo.bytesReceived());
    EXPECT_FALSE(wrappedTcpInfo.bytesRetransmitted());
    EXPECT_FALSE(wrappedTcpInfo.bytesAcked());

    EXPECT_FALSE(wrappedTcpInfo.packetsSent());
    EXPECT_FALSE(wrappedTcpInfo.packetsWithDataSent());
    EXPECT_FALSE(wrappedTcpInfo.packetsReceived());
    EXPECT_FALSE(wrappedTcpInfo.packetsWithDataReceived());
    EXPECT_EQ(
        controlTcpInfo.tcpi_total_retrans,
        wrappedTcpInfo.packetsRetransmitted());

    EXPECT_EQ(
        controlTcpInfo.tcpi_unacked + controlTcpInfo.tcpi_retrans -
            (controlTcpInfo.tcpi_sacked + controlTcpInfo.tcpi_lost),
        wrappedTcpInfo.packetsInFlight());
    EXPECT_NE(0U, wrappedTcpInfo.packetsInFlight());

    EXPECT_EQ(controlTcpInfo.tcpi_snd_cwnd, wrappedTcpInfo.cwndInPackets());
    EXPECT_EQ(
        controlTcpInfo.tcpi_snd_cwnd * controlTcpInfo.tcpi_snd_mss,
        wrappedTcpInfo.cwndInBytes());
    EXPECT_EQ(controlTcpInfo.tcpi_snd_ssthresh, wrappedTcpInfo.ssthresh());
    EXPECT_EQ(controlTcpInfo.tcpi_snd_mss, wrappedTcpInfo.mss());

    EXPECT_FALSE(wrappedTcpInfo.deliveryRateBitsPerSecond());
    EXPECT_FALSE(wrappedTcpInfo.deliveryRateBytesPerSecond());
    EXPECT_FALSE(wrappedTcpInfo.deliveryRateAppLimited());

    // try using getTcpInfoFieldAsOpt to get one of the older fields
    // this field _should_ be available in legacy
    EXPECT_EQ(
        controlTcpInfo.tcpi_snd_cwnd,
        wrappedTcpInfo.getFieldAsOptUInt64(
            &folly::TcpInfo::tcp_info::tcpi_snd_cwnd));

    // try using getTcpInfoFieldAsOpt to get one of the newer fields
    // this field should _not_ be available in legacy
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_info::tcpi_delivery_rate)
            .hasValue());
  }

  static void checkTcpInfoAgainstLatest(const TcpInfo& wrappedTcpInfo) {
    const auto& controlTcpInfo = getTestLatestTcpInfo();

    EXPECT_EQ(us(controlTcpInfo.tcpi_min_rtt), wrappedTcpInfo.minrtt());
    EXPECT_EQ(us(controlTcpInfo.tcpi_rtt), wrappedTcpInfo.srtt());

    EXPECT_EQ(controlTcpInfo.tcpi_bytes_sent, wrappedTcpInfo.bytesSent());
    EXPECT_EQ(
        controlTcpInfo.tcpi_bytes_received, wrappedTcpInfo.bytesReceived());
    EXPECT_EQ(
        controlTcpInfo.tcpi_bytes_retrans, wrappedTcpInfo.bytesRetransmitted());
    EXPECT_EQ(controlTcpInfo.tcpi_bytes_acked, wrappedTcpInfo.bytesAcked());

    EXPECT_EQ(controlTcpInfo.tcpi_segs_out, wrappedTcpInfo.packetsSent());
    EXPECT_EQ(
        controlTcpInfo.tcpi_data_segs_out,
        wrappedTcpInfo.packetsWithDataSent());
    EXPECT_EQ(controlTcpInfo.tcpi_segs_in, wrappedTcpInfo.packetsReceived());
    EXPECT_EQ(
        controlTcpInfo.tcpi_data_segs_in,
        wrappedTcpInfo.packetsWithDataReceived());
    EXPECT_EQ(
        controlTcpInfo.tcpi_total_retrans,
        wrappedTcpInfo.packetsRetransmitted());

    EXPECT_EQ(
        controlTcpInfo.tcpi_unacked + controlTcpInfo.tcpi_retrans -
            (controlTcpInfo.tcpi_sacked + controlTcpInfo.tcpi_lost),
        wrappedTcpInfo.packetsInFlight());
    EXPECT_NE(0U, wrappedTcpInfo.packetsInFlight());

    EXPECT_EQ(controlTcpInfo.tcpi_delivered, wrappedTcpInfo.packetsDelivered());
    EXPECT_EQ(
        controlTcpInfo.tcpi_delivered_ce,
        wrappedTcpInfo.packetsDeliveredWithCEMarks());

    EXPECT_EQ(controlTcpInfo.tcpi_snd_cwnd, wrappedTcpInfo.cwndInPackets());
    EXPECT_EQ(
        controlTcpInfo.tcpi_snd_cwnd * controlTcpInfo.tcpi_snd_mss,
        wrappedTcpInfo.cwndInBytes());
    EXPECT_EQ(controlTcpInfo.tcpi_snd_ssthresh, wrappedTcpInfo.ssthresh());
    EXPECT_EQ(controlTcpInfo.tcpi_snd_mss, wrappedTcpInfo.mss());

    EXPECT_EQ(
        controlTcpInfo.tcpi_delivery_rate * 8,
        wrappedTcpInfo.deliveryRateBitsPerSecond());
    EXPECT_EQ(
        controlTcpInfo.tcpi_delivery_rate,
        wrappedTcpInfo.deliveryRateBytesPerSecond());
    EXPECT_EQ(
        controlTcpInfo.tcpi_delivery_rate_app_limited,
        wrappedTcpInfo.deliveryRateAppLimited());

    // try using getFieldAsOptUInt64 directly
    EXPECT_EQ(
        controlTcpInfo.tcpi_delivery_rate,
        wrappedTcpInfo.getFieldAsOptUInt64(
            &folly::TcpInfo::tcp_info::tcpi_delivery_rate));
  }

  static void checkNoCcNameType(const TcpInfo& wrappedTcpInfo) {
    EXPECT_FALSE(wrappedTcpInfo.ccNameRaw().has_value());
    EXPECT_FALSE(wrappedTcpInfo.ccNameEnum().has_value());
    EXPECT_FALSE(wrappedTcpInfo.ccNameEnumAsStr().has_value());
  }

  static void checkNoCcInfo(const TcpInfo& wrappedTcpInfo) {
    // should get false for all three types
    EXPECT_FALSE(wrappedTcpInfo.bbrCwndGain().has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_bbr_info::bbr_pacing_gain)
            .has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcpvegas_info::tcpv_rtt)
            .has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_dctcp_info::dctcp_alpha)
            .has_value());
  }

  static void checkCcFieldsAgainstBbr(const TcpInfo& wrappedTcpInfo) {
    EXPECT_EQ("BBR", wrappedTcpInfo.ccNameEnumAsStr());
    EXPECT_EQ(TcpInfo::CongestionControlName::BBR, wrappedTcpInfo.ccNameEnum());

    const auto controlCcInfo = getTestBbrInfo().bbr;
    EXPECT_EQ(
        controlCcInfo.bbr_pacing_gain,
        wrappedTcpInfo.getFieldAsOptUInt64(
            &folly::TcpInfo::tcp_bbr_info::bbr_pacing_gain));
    const uint64_t bbrBwBytesPerSecond =
        (uint64_t(controlCcInfo.bbr_bw_hi) << 32) + controlCcInfo.bbr_bw_lo;
    EXPECT_EQ(bbrBwBytesPerSecond, wrappedTcpInfo.bbrBwBytesPerSecond());
    EXPECT_EQ(bbrBwBytesPerSecond * 8, wrappedTcpInfo.bbrBwBitsPerSecond());
    EXPECT_EQ(us(controlCcInfo.bbr_min_rtt), wrappedTcpInfo.bbrMinrtt());
    EXPECT_EQ(controlCcInfo.bbr_pacing_gain, wrappedTcpInfo.bbrPacingGain());
    EXPECT_EQ(controlCcInfo.bbr_cwnd_gain, wrappedTcpInfo.bbrCwndGain());

    // try using getFieldAsOptUInt64 directly to get one of the BBR fields
    EXPECT_EQ(
        controlCcInfo.bbr_pacing_gain,
        wrappedTcpInfo.getFieldAsOptUInt64(
            &folly::TcpInfo::tcp_bbr_info::bbr_pacing_gain));

    // no CC info for the other types
    EXPECT_FALSE(wrappedTcpInfo.getFieldAsOptUInt64(
        &folly::TcpInfo::tcpvegas_info::tcpv_rtt));
    EXPECT_FALSE(wrappedTcpInfo.getFieldAsOptUInt64(
        &folly::TcpInfo::tcp_dctcp_info::dctcp_alpha));
  }

  static void checkCcFieldsAgainstVegas(const TcpInfo& wrappedTcpInfo) {
    EXPECT_EQ("VEGAS", wrappedTcpInfo.ccNameEnumAsStr());
    EXPECT_EQ(
        TcpInfo::CongestionControlName::VEGAS, wrappedTcpInfo.ccNameEnum());

    const auto controlCcInfo = getTestVegasInfo().vegas;
    EXPECT_EQ(
        controlCcInfo.tcpv_rtt,
        wrappedTcpInfo.getFieldAsOptUInt64(
            &folly::TcpInfo::tcpvegas_info::tcpv_rtt));

    // no CC info for the other types
    EXPECT_FALSE(wrappedTcpInfo.bbrCwndGain().has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_bbr_info::bbr_pacing_gain)
            .has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_dctcp_info::dctcp_alpha)
            .has_value());
  }

  static void checkCcFieldsAgainstDctcp(
      const TcpInfo& wrappedTcpInfo,
      const TcpInfo::CongestionControlName dctcpType) {
    switch (dctcpType) {
      case TcpInfo::CongestionControlName::DCTCP:
        EXPECT_EQ("DCTCP", wrappedTcpInfo.ccNameEnumAsStr());
        break;
      case TcpInfo::CongestionControlName::DCTCP_RENO:
        EXPECT_EQ("DCTCP_RENO", wrappedTcpInfo.ccNameEnumAsStr());
        break;
      case TcpInfo::CongestionControlName::DCTCP_CUBIC:
        EXPECT_EQ("DCTCP_CUBIC", wrappedTcpInfo.ccNameEnumAsStr());
        break;
      case TcpInfo::CongestionControlName::UNKNOWN:
      case TcpInfo::CongestionControlName::RENO:
      case TcpInfo::CongestionControlName::CUBIC:
      case TcpInfo::CongestionControlName::BIC:
      case TcpInfo::CongestionControlName::BBR:
      case TcpInfo::CongestionControlName::VEGAS:
      case TcpInfo::CongestionControlName::NumCcTypes:
        FAIL();
    }

    EXPECT_EQ(dctcpType, wrappedTcpInfo.ccNameEnum());

    const auto controlCcInfo = getTestDctcpInfo().dctcp;
    EXPECT_EQ(
        controlCcInfo.dctcp_alpha,
        wrappedTcpInfo.getFieldAsOptUInt64(
            &folly::TcpInfo::tcp_dctcp_info::dctcp_alpha));
    EXPECT_EQ(
        controlCcInfo.dctcp_enabled,
        wrappedTcpInfo.getFieldAsOptUInt64(
            &folly::TcpInfo::tcp_dctcp_info::dctcp_enabled));

    // no CC info for the other types
    EXPECT_FALSE(wrappedTcpInfo.bbrCwndGain().has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcp_bbr_info::bbr_pacing_gain)
            .has_value());
    EXPECT_FALSE(
        wrappedTcpInfo
            .getFieldAsOptUInt64(&folly::TcpInfo::tcpvegas_info::tcpv_rtt)
            .has_value());
  }

  static void checkNoMemoryInfo(const TcpInfo& wrappedTcpInfo) {
    EXPECT_FALSE(wrappedTcpInfo.sendBufInUseBytes().has_value()); // siocoutq
    EXPECT_FALSE(wrappedTcpInfo.recvBufInUseBytes().has_value()); // siocinq
  }

 protected:
  StrictMock<folly::netops::test::MockDispatcher> mockNetOpsDispatcher_;
  StrictMock<TcpInfoTestUtil::MockIoctlDispatcher> mockIoctlDispatcher_;
};

TEST_F(TcpInfoTest, LegacyStruct) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLegacyTcpInfo());

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = false;
  options.getMemInfo = false;
  auto wrappedTcpInfoExpect =
      TcpInfo::initFromFd(s, options, mockNetOpsDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLegacy(wrappedTcpInfo);

  // no CC name/type or info; no memory info
  checkNoCcNameType(wrappedTcpInfo);
  checkNoCcInfo(wrappedTcpInfo);
  checkNoMemoryInfo(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, LatestStruct) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = false;
  options.getMemInfo = false;
  auto wrappedTcpInfoExpect =
      TcpInfo::initFromFd(s, options, mockNetOpsDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);

  // no CC name/type or info; no memory info
  checkNoCcNameType(wrappedTcpInfo);
  checkNoCcInfo(wrappedTcpInfo);
  checkNoMemoryInfo(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, ConstructorWithLatestTcpInfo) {
  TcpInfo wrappedTcpInfo{getTestLatestTcpInfo()};
  checkTcpInfoAgainstLatest(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, LatestStructWithCcInfo) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallCcName(s, "bbr");
  setupExpectCallCcInfo(s, getTestBbrInfo());

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = false;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  checkCcFieldsAgainstBbr(wrappedTcpInfo);

  // no memory info
  checkNoMemoryInfo(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, LatestStructWithMemInfo) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallMemInfo(
      s,
      ExpectCallMemInfoConfig{
          .siocoutq = kTestSiocoutqVal, .siocinq = kTestSiocinqVal});

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = false;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  EXPECT_EQ(kTestSiocoutqVal, wrappedTcpInfo.sendBufInUseBytes()); // siocoutq
  EXPECT_EQ(kTestSiocinqVal, wrappedTcpInfo.recvBufInUseBytes()); // siocinq

  // no CC name/type or info
  checkNoCcNameType(wrappedTcpInfo);
  checkNoCcInfo(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, LatestStructWithCcInfoAndMemInfo) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallCcName(s, "bbr");
  setupExpectCallCcInfo(s, getTestBbrInfo());
  setupExpectCallMemInfo(
      s,
      ExpectCallMemInfoConfig{
          .siocoutq = kTestSiocoutqVal, .siocinq = kTestSiocinqVal});

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  checkCcFieldsAgainstBbr(wrappedTcpInfo);
  EXPECT_EQ(kTestSiocoutqVal, wrappedTcpInfo.sendBufInUseBytes()); // siocoutq
  EXPECT_EQ(kTestSiocinqVal, wrappedTcpInfo.recvBufInUseBytes()); // siocinq
}

TEST_F(TcpInfoTest, LatestStructWithCcInfoAndMemInfoUnknownCc) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallCcName(s, kTestUnknownCcName);
  setupExpectCallMemInfo(
      s,
      ExpectCallMemInfoConfig{
          .siocoutq = kTestSiocoutqVal, .siocinq = kTestSiocinqVal});

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  EXPECT_EQ(kTestSiocoutqVal, wrappedTcpInfo.sendBufInUseBytes()); // siocoutq
  EXPECT_EQ(kTestSiocinqVal, wrappedTcpInfo.recvBufInUseBytes()); // siocinq

  // the CC name and enum should be set, but there should be no other CC info
  EXPECT_EQ(kTestUnknownCcName, wrappedTcpInfo.ccNameRaw());
  EXPECT_EQ("UNKNOWN", wrappedTcpInfo.ccNameEnumAsStr());
  EXPECT_EQ(
      TcpInfo::CongestionControlName::UNKNOWN, wrappedTcpInfo.ccNameEnum());
  checkNoCcInfo(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, FailUninitializedSocket) {
  NetworkSocket s;

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_FALSE(wrappedTcpInfoExpect.hasValue()); // complete failure
}

TEST_F(TcpInfoTest, FailTcpInfo) {
  NetworkSocket s(0);

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  EXPECT_CALL(mockNetOpsDispatcher_, getsockopt(s, IPPROTO_TCP, TCP_INFO, _, _))
      .WillOnce(Return(-1));
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_FALSE(wrappedTcpInfoExpect.hasValue()); // complete failure
}

TEST_F(TcpInfoTest, FailCcName) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallMemInfo(
      s,
      ExpectCallMemInfoConfig{
          .siocoutq = kTestSiocoutqVal, .siocinq = kTestSiocinqVal});

  // ensure that we try to fetch TCP name via TCP_CONGESTION
  // return -1 when trying to get CC name to mimic socket error state
  EXPECT_CALL(
      mockNetOpsDispatcher_,
      getsockopt(
          s,
          IPPROTO_TCP,
          TCP_CONGESTION,
          NotNull(),
          Pointee(Eq(TcpInfo::kLinuxTcpCaNameMax))))
      .WillOnce(Return(-1));

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  EXPECT_EQ(kTestSiocoutqVal, wrappedTcpInfo.sendBufInUseBytes()); // siocoutq
  EXPECT_EQ(kTestSiocinqVal, wrappedTcpInfo.recvBufInUseBytes()); // siocinq

  // no CC name/type or info due to failed lookup for TCP_CONGESTION
  checkNoCcNameType(wrappedTcpInfo);
  checkNoCcInfo(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, FailCcInfo) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallCcName(s, "bbr");
  setupExpectCallMemInfo(
      s,
      ExpectCallMemInfoConfig{
          .siocoutq = kTestSiocoutqVal, .siocinq = kTestSiocinqVal});

  // ensure that we try to fetch TCP info via TCP_CC_INFO
  // return -1 when trying to get CC name to mimic socket error state
  EXPECT_CALL(
      mockNetOpsDispatcher_,
      getsockopt(
          s,
          IPPROTO_TCP,
          TCP_CC_INFO,
          NotNull(),
          Pointee(Eq(sizeof(TcpInfo::tcp_cc_info)))))
      .WillOnce(Return(-1));

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  EXPECT_EQ(kTestSiocoutqVal, wrappedTcpInfo.sendBufInUseBytes()); // siocoutq
  EXPECT_EQ(kTestSiocinqVal, wrappedTcpInfo.recvBufInUseBytes()); // siocinq

  // the CC name and enum should be set, but there should be no other CC info,
  // despite how this is BBR (and thus triggered a TCP_CC_INFO lookup) given
  // that the TCP_CC_INFO lookup failed
  EXPECT_EQ("BBR", wrappedTcpInfo.ccNameEnumAsStr());
  EXPECT_EQ(TcpInfo::CongestionControlName::BBR, wrappedTcpInfo.ccNameEnum());
  checkNoCcInfo(wrappedTcpInfo);
}

TEST_F(TcpInfoTest, FailSiocoutq) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallCcName(s, "bbr");
  setupExpectCallCcInfo(s, getTestBbrInfo());

  EXPECT_CALL(mockIoctlDispatcher_, ioctl(s.toFd(), SIOCOUTQ, testing::_))
      .WillOnce(Return(-1));
  EXPECT_CALL(mockIoctlDispatcher_, ioctl(s.toFd(), SIOCINQ, testing::_))
      .WillOnce(WithArgs<2>(Invoke([toSet = kTestSiocinqVal](void* argp) {
        size_t* val = static_cast<size_t*>(argp);
        *val = toSet;
        return 0;
      })));

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  checkCcFieldsAgainstBbr(wrappedTcpInfo);

  // should have SIOCINQ, but no SIOCOUTQ given failure during lookup
  EXPECT_FALSE(wrappedTcpInfo.sendBufInUseBytes().has_value()); // siocoutq
  EXPECT_EQ(kTestSiocinqVal, wrappedTcpInfo.recvBufInUseBytes()); // siocinq
}

TEST_F(TcpInfoTest, FailSiocinq) {
  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallCcName(s, "bbr");
  setupExpectCallCcInfo(s, getTestBbrInfo());

  EXPECT_CALL(mockIoctlDispatcher_, ioctl(s.toFd(), SIOCOUTQ, testing::_))
      .WillOnce(WithArgs<2>(Invoke([toSet = kTestSiocoutqVal](void* argp) {
        size_t* val = static_cast<size_t*>(argp);
        *val = toSet;
        return 0;
      })));
  EXPECT_CALL(mockIoctlDispatcher_, ioctl(s.toFd(), SIOCINQ, testing::_))
      .WillOnce(Return(-1));

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  checkCcFieldsAgainstBbr(wrappedTcpInfo);

  // should have SIOCOUTQ, but no SIOCINQ given failure during lookup
  EXPECT_EQ(kTestSiocoutqVal, wrappedTcpInfo.sendBufInUseBytes()); // siocoutq
  EXPECT_FALSE(wrappedTcpInfo.recvBufInUseBytes().has_value()); // siocinq
}

struct TcpInfoTestCcParamRawStrAndEnum {
  TcpInfoTestCcParamRawStrAndEnum(
      std::string ccNameRaw, TcpInfo::CongestionControlName ccNameEnum)
      : ccNameRaw(std::move(ccNameRaw)), ccNameEnum(ccNameEnum) {}
  const std::string ccNameRaw; // raw name returned by kernel
  const TcpInfo::CongestionControlName ccNameEnum; // expected
};

class TcpInfoTestCcParam
    : public TcpInfoTest,
      public testing::WithParamInterface<TcpInfoTestCcParamRawStrAndEnum> {
 public:
  static std::vector<TcpInfoTestCcParamRawStrAndEnum> getTestingValues() {
    return std::vector<TcpInfoTestCcParamRawStrAndEnum>{
        {kTestUnknownCcName, TcpInfo::CongestionControlName::UNKNOWN},
        {"cubic", TcpInfo::CongestionControlName::CUBIC},
        {"bic", TcpInfo::CongestionControlName::BIC},
        {"dctcp", TcpInfo::CongestionControlName::DCTCP},
        {"dctcp_reno", TcpInfo::CongestionControlName::DCTCP_RENO},
        {"bbr", TcpInfo::CongestionControlName::BBR},
        {"reno", TcpInfo::CongestionControlName::RENO},
        {"dctcp_cubic", TcpInfo::CongestionControlName::DCTCP_CUBIC},
        {"vegas", TcpInfo::CongestionControlName::VEGAS}};
  }
};

INSTANTIATE_TEST_SUITE_P(
    CcParamTests,
    TcpInfoTestCcParam,
    ::testing::ValuesIn(TcpInfoTestCcParam::getTestingValues()));

TEST_P(TcpInfoTestCcParam, FetchAllAndCheck) {
  const auto testParams = GetParam();

  NetworkSocket s(0);
  setupExpectCallTcpInfo(s, getTestLatestTcpInfo());
  setupExpectCallMemInfo(
      s,
      ExpectCallMemInfoConfig{
          .siocoutq = kTestSiocoutqVal, .siocinq = kTestSiocinqVal});

  // setup CC specifics for the test
  setupExpectCallCcName(s, testParams.ccNameRaw);
  switch (testParams.ccNameEnum) {
    case TcpInfo::CongestionControlName::UNKNOWN:
    case TcpInfo::CongestionControlName::RENO:
    case TcpInfo::CongestionControlName::CUBIC:
    case TcpInfo::CongestionControlName::BIC:
      break; // CC_INFO not supported
    case TcpInfo::CongestionControlName::BBR:
      setupExpectCallCcInfo(s, getTestBbrInfo());
      break;
    case TcpInfo::CongestionControlName::VEGAS:
      setupExpectCallCcInfo(s, getTestVegasInfo());
      break;
    case TcpInfo::CongestionControlName::DCTCP:
    case TcpInfo::CongestionControlName::DCTCP_RENO:
    case TcpInfo::CongestionControlName::DCTCP_CUBIC:
      setupExpectCallCcInfo(s, getTestDctcpInfo());
      break;
    case TcpInfo::CongestionControlName::NumCcTypes:
      FAIL();
  }

  TcpInfo::LookupOptions options = {};
  options.getCcInfo = true;
  options.getMemInfo = true;
  auto wrappedTcpInfoExpect = TcpInfo::initFromFd(
      s, options, mockNetOpsDispatcher_, mockIoctlDispatcher_);
  ASSERT_TRUE(wrappedTcpInfoExpect.hasValue());
  const auto& wrappedTcpInfo = wrappedTcpInfoExpect.value();

  checkTcpInfoAgainstLatest(wrappedTcpInfo);
  EXPECT_EQ(kTestSiocoutqVal, wrappedTcpInfo.sendBufInUseBytes()); // siocoutq
  EXPECT_EQ(kTestSiocinqVal, wrappedTcpInfo.recvBufInUseBytes()); // siocinq

  // check CC information
  EXPECT_EQ(testParams.ccNameRaw, wrappedTcpInfo.ccNameRaw());
  EXPECT_EQ(testParams.ccNameEnum, wrappedTcpInfo.ccNameEnum());
  switch (testParams.ccNameEnum) {
    case TcpInfo::CongestionControlName::UNKNOWN:
      EXPECT_EQ("UNKNOWN", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::RENO:
      EXPECT_EQ("RENO", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::CUBIC:
      EXPECT_EQ("CUBIC", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::BIC:
      EXPECT_EQ("BIC", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::BBR:
      EXPECT_EQ("BBR", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::VEGAS:
      EXPECT_EQ("VEGAS", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::DCTCP:
      EXPECT_EQ("DCTCP", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::DCTCP_RENO:
      EXPECT_EQ("DCTCP_RENO", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::DCTCP_CUBIC:
      EXPECT_EQ("DCTCP_CUBIC", wrappedTcpInfo.ccNameEnumAsStr());
      break;
    case TcpInfo::CongestionControlName::NumCcTypes:
      FAIL();
  }

  switch (testParams.ccNameEnum) {
    case TcpInfo::CongestionControlName::UNKNOWN:
    case TcpInfo::CongestionControlName::RENO:
    case TcpInfo::CongestionControlName::CUBIC:
    case TcpInfo::CongestionControlName::BIC:
      checkNoCcInfo(wrappedTcpInfo); // should be no extra CC_INFO
      break;
    case TcpInfo::CongestionControlName::BBR:
      checkCcFieldsAgainstBbr(wrappedTcpInfo);
      break;
    case TcpInfo::CongestionControlName::VEGAS:
      checkCcFieldsAgainstVegas(wrappedTcpInfo);
      break;
    case TcpInfo::CongestionControlName::DCTCP:
    case TcpInfo::CongestionControlName::DCTCP_RENO:
    case TcpInfo::CongestionControlName::DCTCP_CUBIC:
      checkCcFieldsAgainstDctcp(wrappedTcpInfo, testParams.ccNameEnum);
      break;
    case TcpInfo::CongestionControlName::NumCcTypes:
      FAIL();
  }
}

#endif // #ifdef __linux__
