/*
 *  Copyright 2024 The WebRTC Project Authors. All rights reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include <cstdint>
#include <ctime>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "absl/flags/flag.h"
#include "api/dtls_transport_interface.h"
#include "api/ice_transport_interface.h"
#include "api/numerics/samples_stats_counter.h"
#include "api/test/rtc_error_matchers.h"
#include "api/units/data_rate.h"
#include "api/units/time_delta.h"
#include "p2p/base/connection_info.h"
#include "p2p/base/ice_transport_internal.h"
#include "p2p/dtls/dtls_ice_integration_fixture.h"
#include "p2p/dtls/dtls_transport.h"
#include "rtc_base/async_packet_socket.h"
#include "rtc_base/logging.h"
#include "rtc_base/random.h"
#include "rtc_base/socket_address.h"
#include "rtc_base/ssl_stream_adapter.h"
#include "test/gmock.h"
#include "test/gtest.h"
#include "test/wait_until.h"

ABSL_FLAG(int32_t,
          long_running_seed,
          7788,
          "0 means use time(0) as seed (i.e non deterministic)");
ABSL_FLAG(int32_t,
          long_running_run_time_minutes,
          7,
          "Runtime in simulated time.");
ABSL_FLAG(bool, long_running_send_data, true, "");

namespace webrtc {
namespace {

using dtls_ice_integration_fixture::TestConfig;
using ::testing::Eq;
using ::testing::IsTrue;
using ::testing::Not;

class DtlsIceIntegrationTest : public dtls_ice_integration_fixture::Base,
                               public ::testing::TestWithParam<TestConfig> {
 public:
  DtlsIceIntegrationTest() : dtls_ice_integration_fixture::Base(GetParam()) {}
  ~DtlsIceIntegrationTest() override = default;

  void SetUp() override { Base::SetUp(); }
  void TearDown() override { Base::TearDown(); }

  static int CountConnectionsWithFilter(
      IceTransportInternal* ice,
      std::function<bool(const ConnectionInfo&)> filter) {
    IceTransportStats stats;
    ice->GetStats(&stats);
    int count = 0;
    for (const auto& con : stats.connection_infos) {
      if (filter(con)) {
        count++;
      }
    }
    return count;
  }

  static int CountConnections(IceTransportInternal* ice) {
    return CountConnectionsWithFilter(ice, [](auto con) { return true; });
  }

  static int CountWritableConnections(IceTransportInternal* ice) {
    return CountConnectionsWithFilter(ice,
                                      [](auto con) { return con.writable; });
  }

  void CheckRetransmissions() {
    if (!server_.config.ice_lite) {
      EXPECT_EQ(client_.dtls->GetRetransmissionCount(), 0);
      EXPECT_EQ(server_.dtls->GetRetransmissionCount(), 0);
      return;
    }
    if (client_.config.ssl_role == SSL_CLIENT) {
      EXPECT_EQ(client_.dtls->GetRetransmissionCount(), 0);
      EXPECT_EQ(server_.dtls->GetRetransmissionCount(), 0);
      return;
    }

    // TODO: bugs.webrtc.org/367395350 - Investigate retransmissions
    // from in fake ice lite scenarios. Very few remaining!
    // - server is (fake) ICE lite but SSL_CLIENT
    if (client_.config.dtls_in_stun == server_.config.dtls_in_stun) {
      const bool pqc = client_.config.pqc && server_.config.pqc;
      EXPECT_EQ(client_.dtls->GetRetransmissionCount(), 0);
      EXPECT_LE(server_.dtls->GetRetransmissionCount(), !pqc ? 0 : 1);
      return;
    }

    EXPECT_LE(client_.dtls->GetRetransmissionCount(), 1);
    EXPECT_LE(server_.dtls->GetRetransmissionCount(), 1);
  }
};

TEST_P(DtlsIceIntegrationTest, SmokeTest) {
  ConfigureEmulatedNetwork(/* pct_loss= */ 0);
  Prepare();
  client_thread()->PostTask([&]() { client_.ice()->MaybeStartGathering(); });
  server_thread()->PostTask([&]() { server_.ice()->MaybeStartGathering(); });

  // Note: this only reaches the pending piggybacking state.
  EXPECT_THAT(
      WaitUntil(
          [&] { return client_.dtls->writable() && server_.dtls->writable(); },
          IsTrue(), wait_until_settings()),
      IsRtcOk());

  client_thread()->BlockingCall([&]() {
    EXPECT_EQ(client_.dtls->IsDtlsPiggybackSupportedByPeer(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
    EXPECT_EQ(client_.dtls->WasDtlsCompletedByPiggybacking(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  });
  server_thread()->BlockingCall([&]() {
    EXPECT_EQ(server_.dtls->IsDtlsPiggybackSupportedByPeer(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
    EXPECT_EQ(server_.dtls->WasDtlsCompletedByPiggybacking(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  });

  if (client_.config.dtls_in_stun && server_.config.dtls_in_stun) {
    EXPECT_GE(dtls_client().dtls->GetStunDataCount(), 0);
    EXPECT_GE(dtls_server().dtls->GetStunDataCount(), 0);
  }

  CheckRetransmissions();
}

TEST_P(DtlsIceIntegrationTest, AddCandidates) {
  Prepare();
  client_.ice()->MaybeStartGathering();
  server_.ice()->MaybeStartGathering();

  // Note: this only reaches the pending piggybacking state.
  EXPECT_THAT(
      WaitUntil(
          [&] { return client_.dtls->writable() && server_.dtls->writable(); },
          IsTrue(), wait_until_settings()),
      IsRtcOk());
  EXPECT_EQ(client_.dtls->IsDtlsPiggybackSupportedByPeer(),
            client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  EXPECT_EQ(server_.dtls->IsDtlsPiggybackSupportedByPeer(),
            client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  EXPECT_EQ(client_.dtls->WasDtlsCompletedByPiggybacking(),
            client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  EXPECT_EQ(server_.dtls->WasDtlsCompletedByPiggybacking(),
            client_.config.dtls_in_stun && server_.config.dtls_in_stun);

  if (client_.config.dtls_in_stun && server_.config.dtls_in_stun) {
    EXPECT_GE(dtls_client().dtls->GetStunDataCount(), 0);
    EXPECT_GE(dtls_server().dtls->GetStunDataCount(), 0);
  }

  // Validate that we can add new Connections (that become writable).
  network_manager_->AddInterface(SocketAddress("192.168.2.1", 0));
  EXPECT_THAT(WaitUntil(
                  [&] {
                    return CountWritableConnections(client_.ice()) > 1 &&
                           CountWritableConnections(server_.ice()) > 1;
                  },
                  IsTrue(), wait_until_settings()),
              IsRtcOk());
}

// Check that DtlsInStun still works even if SetRemoteFingerprint is called
// "late". This is what happens if the answer sdp comes strictly after ICE has
// connected. Before this patch, this would disable stun-piggy-backing.
TEST_P(DtlsIceIntegrationTest, ClientLateCertificate) {
  client_.store_but_dont_set_remote_fingerprint = true;
  ConfigureEmulatedNetwork(/* pct_loss= */ 0);
  Prepare();
  client_thread()->PostTask([&]() { client_.ice()->MaybeStartGathering(); });
  server_thread()->PostTask([&]() { server_.ice()->MaybeStartGathering(); });

  ASSERT_THAT(WaitUntil(
                  [&] {
                    return client_thread()->BlockingCall([&]() {
                      return CountWritableConnections(client_.ice());
                    }) > 0;
                  },
                  IsTrue(), wait_until_settings()),
              IsRtcOk());

  client_thread()->BlockingCall([&]() { SetRemoteFingerprint(client_); });

  ASSERT_THAT(
      WaitUntil(
          [&] { return client_.dtls->writable() && server_.dtls->writable(); },
          IsTrue(), wait_until_settings()),
      IsRtcOk());

  client_thread()->BlockingCall([&]() {
    EXPECT_EQ(client_.dtls->IsDtlsPiggybackSupportedByPeer(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
    EXPECT_EQ(client_.dtls->WasDtlsCompletedByPiggybacking(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  });
  server_thread()->BlockingCall([&]() {
    EXPECT_EQ(server_.dtls->WasDtlsCompletedByPiggybacking(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  });

  CheckRetransmissions();
}

TEST_P(DtlsIceIntegrationTest, TestWithPacketLoss) {
  if (!IsBoringSsl()) {
    GTEST_SKIP() << "Needs boringssl.";
  }

  ConfigureEmulatedNetwork();
  Prepare();

  client_thread()->PostTask([&]() { client_.ice()->MaybeStartGathering(); });
  server_thread()->PostTask([&]() { server_.ice()->MaybeStartGathering(); });

  EXPECT_THAT(WaitUntil(
                  [&] {
                    return client_thread()->BlockingCall([&]() {
                      return client_.dtls->writable();
                    }) && server_thread()->BlockingCall([&]() {
                      return server_.dtls->writable();
                    });
                  },
                  IsTrue(), wait_until_settings()),
              IsRtcOk());

  EXPECT_EQ(client_thread()->BlockingCall([&]() {
    return client_.dtls->IsDtlsPiggybackSupportedByPeer();
  }),
            client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  EXPECT_EQ(server_thread()->BlockingCall([&]() {
    return server_.dtls->IsDtlsPiggybackSupportedByPeer();
  }),
            client_.config.dtls_in_stun && server_.config.dtls_in_stun);
}

TEST_P(DtlsIceIntegrationTest, LongRunningTestWithPacketLoss) {
  if (!IsBoringSsl()) {
    GTEST_SKIP() << "Needs boringssl.";
  }

  int seed = absl::GetFlag(FLAGS_long_running_seed);
  if (seed == 0) {
    seed = 1 + time(0);
  }
  RTC_LOG(LS_INFO) << "seed: " << seed;
  Random rand(seed);
  ConfigureEmulatedNetwork();
  Prepare();

  client_thread()->PostTask([&]() { client_.ice()->MaybeStartGathering(); });
  server_thread()->PostTask([&]() { server_.ice()->MaybeStartGathering(); });

  ASSERT_THAT(WaitUntil(
                  [&] {
                    return client_thread()->BlockingCall([&]() {
                      return client_.dtls->writable();
                    }) && server_thread()->BlockingCall([&]() {
                      return server_.dtls->writable();
                    });
                  },
                  IsTrue(), wait_until_settings()),
              IsRtcOk());

  auto now = CurrentTime();
  auto end = now + TimeDelta::Minutes(
                       absl::GetFlag(FLAGS_long_running_run_time_minutes));
  int client_sent = 0;
  int client_recv = 0;
  int server_sent = 0;
  int server_recv = 0;
  void* id = this;
  client_thread()->BlockingCall([&]() {
    return client_.dtls->RegisterReceivedPacketCallback(
        id, [&](auto, auto) { client_recv++; });
  });
  server_thread()->BlockingCall([&]() {
    return server_.dtls->RegisterReceivedPacketCallback(
        id, [&](auto, auto) { server_recv++; });
  });
  while (now < end) {
    int delay = static_cast<int>(rand.Gaussian(100, 25));
    if (delay < 25) {
      delay = 25;
    }

    AdvanceTime(TimeDelta::Millis(delay));
    now = CurrentTime();

    if (absl::GetFlag(FLAGS_long_running_send_data)) {
      int flags = 0;
      AsyncSocketPacketOptions options;
      std::string a_long_string(500, 'a');
      if (client_thread()->BlockingCall([&]() {
            return client_.dtls->SendPacket(
                a_long_string.c_str(), a_long_string.length(), options, flags);
          }) > 0) {
        client_sent++;
      }
      if (server_thread()->BlockingCall([&]() {
            return server_.dtls->SendPacket(
                a_long_string.c_str(), a_long_string.length(), options, flags);
          }) > 0) {
        server_sent++;
      }
    }

    EXPECT_THAT(WaitUntil(
                    [&] {
                      return client_thread()->BlockingCall([&]() {
                        return client_.dtls->writable();
                      }) && server_thread()->BlockingCall([&]() {
                        return server_.dtls->writable();
                      });
                    },
                    IsTrue(), wait_until_settings()),
                IsRtcOk());
    ASSERT_THAT(client_thread()->BlockingCall(
                    [&]() { return client_.dtls->dtls_state(); }),
                Not(Eq(DtlsTransportState::kFailed)));
    ASSERT_THAT(server_thread()->BlockingCall(
                    [&]() { return server_.dtls->dtls_state(); }),
                Not(Eq(DtlsTransportState::kFailed)));
  }

  client_thread()->BlockingCall(
      [&]() { return client_.dtls->DeregisterReceivedPacketCallback(id); });
  server_thread()->BlockingCall(
      [&]() { return server_.dtls->DeregisterReceivedPacketCallback(id); });

  RTC_LOG(LS_INFO) << "Server sent " << server_sent << " packets "
                   << " client received: " << client_recv << " ("
                   << (client_recv * 100 / (1 + server_sent)) << "%)";
  RTC_LOG(LS_INFO) << "Client sent " << client_sent << " packets "
                   << " server received: " << server_recv << " ("
                   << (server_recv * 100 / (1 + client_sent)) << "%)";
}

// Verify that DtlsStunPiggybacking works even if one (or several)
// of the STUN_BINDING_REQUESTs are so full that dtls does not fit.
TEST_P(DtlsIceIntegrationTest, AlmostFullSTUN_BINDING) {
  ConfigureEmulatedNetwork(/* pct_loss= */ 0);
  Prepare();

  std::string a_long_string(500, 'a');
  client_.ice()->GetDictionaryWriter()->get().SetByteString(77)->CopyBytes(
      a_long_string);
  server_.ice()->GetDictionaryWriter()->get().SetByteString(78)->CopyBytes(
      a_long_string);

  client_thread()->PostTask([&]() { client_.ice()->MaybeStartGathering(); });
  server_thread()->PostTask([&]() { server_.ice()->MaybeStartGathering(); });

  // Note: this only reaches the pending piggybacking state.
  EXPECT_THAT(
      WaitUntil(
          [&] { return client_.dtls->writable() && server_.dtls->writable(); },
          IsTrue(), wait_until_settings()),
      IsRtcOk());

  client_thread()->BlockingCall([&]() {
    EXPECT_EQ(client_.dtls->IsDtlsPiggybackSupportedByPeer(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
    EXPECT_EQ(client_.dtls->WasDtlsCompletedByPiggybacking(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  });

  server_thread()->BlockingCall([&]() {
    EXPECT_EQ(server_.dtls->IsDtlsPiggybackSupportedByPeer(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
    EXPECT_EQ(server_.dtls->WasDtlsCompletedByPiggybacking(),
              client_.config.dtls_in_stun && server_.config.dtls_in_stun);
  });

  if (client_.config.dtls_in_stun && server_.config.dtls_in_stun &&
      (client_.config.pqc || server_.config.pqc)) {
    // TODO: bugs.webrtc.org/367395350 - Investigate why there is
    // retransmissions in the scenario where the STUN packet is almost full,
    // e.g. will the issue be solved by our effort to smooth BoringSSL packets ?
    EXPECT_LE(client_.dtls->GetRetransmissionCount(), 1);
    EXPECT_LE(server_.dtls->GetRetransmissionCount(), 1);
  } else {
    CheckRetransmissions();
  }
}

INSTANTIATE_TEST_SUITE_P(DtlsStunPiggybackingIntegrationTest,
                         DtlsIceIntegrationTest,
                         ::testing::ValuesIn(TestConfig::AllVariants()));

struct DtlsIceIntegrationPerformanceTest
    : public ::testing::TestWithParam<TestConfig> {
  DtlsIceIntegrationPerformanceTest() {}
  ~DtlsIceIntegrationPerformanceTest() override = default;

  void SetUp() override {}
  void TearDown() override {}

#ifdef NDEBUG
  static constexpr int kLossVariants[] = {0, 5, 10, 15};
#else
  // Only run 1 variant to not consume too much time
  static constexpr int kLossVariants[] = {10};
#endif

  static std::vector<TestConfig> Variants() {
    std::vector<TestConfig> out;
    for (auto loss : kLossVariants) {
      for (auto base : TestConfig::AllVariants()) {
        auto config = base;
        config.pct_loss = loss;
        config.client_interface_count = 1;
        config.server_interface_count = 1;
        config.fix();

        // Let's skip a few combinations...
        if (config.client_config.pqc != config.server_config.pqc) {
          continue;
        }

        // We only need to emit the 1 dtls-in-stun variant
        // since we iterate over it inside the actual test.
        if (!(config.client_config.dtls_in_stun == false &&
              config.server_config.dtls_in_stun == false)) {
          continue;
        }

        out.push_back(config);
      }
    }
    return out;
  }

  bool LessThan(SamplesStatsCounter& s1, SamplesStatsCounter& s2) {
    EXPECT_LE(s1.GetAverage(), s2.GetAverage());
    EXPECT_LE(s1.GetPercentile(0.10), s2.GetPercentile(0.10));
    EXPECT_LE(s1.GetPercentile(0.50), s2.GetPercentile(0.50));
    EXPECT_LE(s1.GetPercentile(0.95), s2.GetPercentile(0.95));
    return true;
  }
};

INSTANTIATE_TEST_SUITE_P(
    DtlsIceIntegrationPerformanceTest,
    DtlsIceIntegrationPerformanceTest,
    ::testing::ValuesIn(DtlsIceIntegrationPerformanceTest::Variants()));

TEST_P(DtlsIceIntegrationPerformanceTest, ConnectTime) {
  if (!dtls_ice_integration_fixture::Base::IsBoringSsl()) {
    GTEST_SKIP() << "Needs boringssl.";
  }

  int iter = 50;
  // Use a fixed seed to get consistent behavior.
  Random rand(/* seed= */ 77);
  std::vector<SamplesStatsCounter> stats_results;
  {
    TestConfig config = GetParam();
    config.client_config.dtls_in_stun = false;
    config.server_config.dtls_in_stun = false;
    dtls_ice_integration_fixture::Base base(config);
    stats_results.push_back(base.RunBenchmark(iter));
  }

  // Verify that turning ON should give better
  // result than having it OFF.
  {
    TestConfig config = GetParam();
    config.client_config.dtls_in_stun = true;
    config.server_config.dtls_in_stun = true;
    dtls_ice_integration_fixture::Base base(config);
    auto result = base.RunBenchmark(iter);
    EXPECT_TRUE(LessThan(result, stats_results[0]));
  }
}

}  // namespace
}  // namespace webrtc
