#include "common.hpp"
#include "RTC/RemoteClockOffsetEstimator.hpp"
#include <catch2/catch_test_macros.hpp>

SCENARIO("RemoteClockOffsetEstimator", "[rtp][rtcp][remoteclockoffsetestimator]")
{
	// Remote sender wall clock, as reported in the NTP field of its Sender Reports
	// (microseconds since 1900).
	constexpr int64_t RemoteBaseUs{ 3976000000000 * 1000 };
	// Our own monotonic clock.
	constexpr int64_t LocalBaseUs{ 1000000 * 1000 };
	// Offset expected when Sender Reports reach us with no delay at all.
	constexpr int64_t BaseOffsetUs{ LocalBaseUs - RemoteBaseUs };

	const auto minSampleCount = RTC::RemoteClockOffsetEstimator::MinSampleCount;
	const auto windowSize     = RTC::RemoteClockOffsetEstimator::WindowSize;

	// Feeds `count` Sender Reports one second apart, each one reaching us `owdUs`
	// after having been generated by the sender.
	//
	// NOTE: The RTT is given in milliseconds, being that the unit of the argument.
	auto feed = [](
	              RTC::RemoteClockOffsetEstimator& estimator,
	              int64_t remoteNtpUs,
	              int64_t localArrivalAtUs,
	              size_t count,
	              int64_t owdUs,
	              int64_t rttMs) -> void
	{
		for (size_t idx{ 0 }; idx < count; ++idx)
		{
			const int64_t elapsedUs = static_cast<int64_t>(idx) * 1000000;

			estimator.AddSenderReport(remoteNtpUs + elapsedUs, localArrivalAtUs + elapsedUs + owdUs, rttMs);
		}
	};

	RTC::RemoteClockOffsetEstimator estimator;

	SECTION("no offset until MinSampleCount Sender Reports have been received")
	{
		feed(estimator, RemoteBaseUs, LocalBaseUs, minSampleCount - 1, 0, 0);

		REQUIRE_FALSE(estimator.GetOffsetUs().has_value());

		// One more Sender Report reaches the minimum.
		const int64_t elapsedUs = static_cast<int64_t>(minSampleCount - 1) * 1000000;

		feed(estimator, RemoteBaseUs + elapsedUs, LocalBaseUs + elapsedUs, 1, 0, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs);
	}

	SECTION("the offset carries the Sender Report one way delay when the RTT is unknown")
	{
		feed(estimator, RemoteBaseUs, LocalBaseUs, minSampleCount, 40 * 1000, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs + (40 * 1000));
	}

	SECTION("half of the RTT is removed from the offset")
	{
		feed(estimator, RemoteBaseUs, LocalBaseUs, minSampleCount, 40 * 1000, 80);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs);
	}

	SECTION("a single delayed Sender Report does not move the offset")
	{
		feed(estimator, RemoteBaseUs, LocalBaseUs, windowSize, 20 * 1000, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs + (20 * 1000));

		// A Sender Report that took much longer to reach us must be rejected by the
		// median.
		const int64_t elapsedUs = static_cast<int64_t>(windowSize) * 1000000;

		feed(estimator, RemoteBaseUs + elapsedUs, LocalBaseUs + elapsedUs, 1, 500 * 1000, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs + (20 * 1000));
	}

	SECTION("the offset follows a sustained change in delay")
	{
		feed(estimator, RemoteBaseUs, LocalBaseUs, windowSize, 0, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs);

		// Once the whole window has been replaced the new delay is fully reflected.
		const int64_t elapsedUs = static_cast<int64_t>(windowSize) * 1000000;

		feed(estimator, RemoteBaseUs + elapsedUs, LocalBaseUs + elapsedUs, windowSize, 100 * 1000, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs + (100 * 1000));
	}

	SECTION("delayed Sender Reports move the offset once they are the majority")
	{
		feed(estimator, RemoteBaseUs, LocalBaseUs, windowSize, 0, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs);

		// Less than half of the window delayed, so the median still falls on a
		// Sender Report that reached us with no delay.
		int64_t elapsedUs = static_cast<int64_t>(windowSize) * 1000000;

		feed(estimator, RemoteBaseUs + elapsedUs, LocalBaseUs + elapsedUs, windowSize / 2, 300 * 1000, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs);

		// One more delayed Sender Report makes them the majority.
		elapsedUs += static_cast<int64_t>(windowSize / 2) * 1000000;

		feed(estimator, RemoteBaseUs + elapsedUs, LocalBaseUs + elapsedUs, 1, 300 * 1000, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs + (300 * 1000));
	}

	SECTION("the offset tracks a drifting remote clock")
	{
		// The remote wall clock runs one millisecond per second slower than ours, so
		// each Sender Report yields an offset one millisecond higher than the
		// previous one.
		for (size_t idx{ 0 }; idx < windowSize; ++idx)
		{
			estimator.AddSenderReport(
			  RemoteBaseUs + (static_cast<int64_t>(idx) * 1000000),
			  LocalBaseUs + (static_cast<int64_t>(idx) * 1001000),
			  0);
		}

		REQUIRE(estimator.GetOffsetUs().has_value());
		// The samples in the window grow by one millisecond each, so the median is
		// the middle of that ramp.
		REQUIRE(
		  // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		  estimator.GetOffsetUs().value() == BaseOffsetUs + (static_cast<int64_t>(windowSize / 2) * 1000));

		// The drift keeps going, so the offset keeps up with it rather than sticking
		// to the value it already had.
		for (size_t idx{ windowSize }; idx < windowSize * 2; ++idx)
		{
			estimator.AddSenderReport(
			  RemoteBaseUs + (static_cast<int64_t>(idx) * 1000000),
			  LocalBaseUs + (static_cast<int64_t>(idx) * 1001000),
			  0);
		}

		REQUIRE(estimator.GetOffsetUs().has_value());
		REQUIRE(
		  // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		  estimator.GetOffsetUs().value() ==
		  BaseOffsetUs + (static_cast<int64_t>(windowSize + (windowSize / 2)) * 1000));
	}

	SECTION("all the Sender Reports of a compound packet produce a single sample")
	{
		// Three Sender Reports of a same compound packet, hence a single arrival
		// time.
		estimator.AddSenderReport(RemoteBaseUs, LocalBaseUs, 0);
		estimator.AddSenderReport(RemoteBaseUs + 1000, LocalBaseUs, 0);
		estimator.AddSenderReport(RemoteBaseUs + 2000, LocalBaseUs, 0);

		REQUIRE_FALSE(estimator.GetOffsetUs().has_value());

		// Two more compound packets reach the minimum.
		estimator.AddSenderReport(RemoteBaseUs + 1000000, LocalBaseUs + 1000000, 0);
		estimator.AddSenderReport(RemoteBaseUs + 1001000, LocalBaseUs + 1000000, 0);
		estimator.AddSenderReport(RemoteBaseUs + 2000000, LocalBaseUs + 2000000, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs);
	}

	SECTION("Sender Reports with no NTP timestamp are ignored")
	{
		// NOTE: Not using feed() here since it would advance the NTP timestamp.
		estimator.AddSenderReport(0, LocalBaseUs, 0);
		estimator.AddSenderReport(0, LocalBaseUs + 1000000, 0);
		estimator.AddSenderReport(0, LocalBaseUs + 2000000, 0);

		REQUIRE_FALSE(estimator.GetOffsetUs().has_value());
	}

	SECTION("remote times are translated into our clock")
	{
		const RTC::RemoteClockOffsetEstimator untrainedEstimator;

		REQUIRE_FALSE(untrainedEstimator.RemoteUsToLocalUs(RemoteBaseUs).has_value());

		feed(estimator, RemoteBaseUs, LocalBaseUs, minSampleCount, 0, 0);

		REQUIRE(estimator.RemoteUsToLocalUs(RemoteBaseUs).has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.RemoteUsToLocalUs(RemoteBaseUs).value() == LocalBaseUs);
		REQUIRE(estimator.RemoteUsToLocalUs(RemoteBaseUs + 500000).has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.RemoteUsToLocalUs(RemoteBaseUs + 500000).value() == LocalBaseUs + 500000);

		// A remote time older than the origin of our clock cannot be translated.
		REQUIRE_FALSE(estimator.RemoteUsToLocalUs(0).has_value());
	}

	SECTION("Reset() clears the estimation")
	{
		feed(estimator, RemoteBaseUs, LocalBaseUs, minSampleCount, 0, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());

		estimator.Reset();

		REQUIRE_FALSE(estimator.GetOffsetUs().has_value());

		// The last arrival time is forgotten as well, so Sender Reports arriving at
		// the very same times as before the reset are not taken as duplicates.
		feed(estimator, RemoteBaseUs, LocalBaseUs, minSampleCount, 0, 0);

		REQUIRE(estimator.GetOffsetUs().has_value());
		// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
		REQUIRE(estimator.GetOffsetUs().value() == BaseOffsetUs);
	}
}
