#include "test_common.h"

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <string>
#include <random>
#include <chrono>
#include <thread>

#include <steam/steamnetworkingsockets.h>
#include <steam/isteamnetworkingutils.h>
#ifndef STEAMNETWORKINGSOCKETS_OPENSOURCE
#include <steam/steam_api.h>
#endif

// It's 2021 and the C language doesn't have a cross-platform way to
// compare strings in a case-insensitive way
#ifdef _MSC_VER
	#define strcasecmp(a,b) stricmp(a,b)
#endif

enum class ETestConnectionMode
{
	Cursory, // Very fast sanity check: 1 condition, short durations
	Normal,  // Standard: 2 conditions, moderate durations
	Soak,    // Exhaustive: all conditions, long durations
};

static std::default_random_engine g_rand;
static SteamNetworkingMicroseconds g_usecTestElapsed;

static HSteamListenSocket g_hSteamListenSocket = k_HSteamListenSocket_Invalid;

struct TestMsg
{
	int64 m_nMsgNum;
	SteamNetworkingMicroseconds m_usecWhenSent;
	bool m_bReliable;
	int m_cbSize;
	static constexpr int k_cbMaxSize = 10*1000;
	uint8 m_data[ k_cbMaxSize ];
};

struct SFakePeer
{
	SFakePeer( const char *pName )
	: m_sName( pName )
	{
		Reset();
	}

	std::string m_sName;
	int64 m_nReliableSendMsgCount;
	int64 m_nUnreliableSendMsgCount;
	int64 m_nReliableExpectedRecvMsg;
	int64 m_nExpectedRecvMsg;
	float m_flReliableMsgDelay;
	float m_flUnreliableMsgDelay;
	HSteamNetConnection m_hSteamNetConnection;
	bool m_bIsConnected;
	int m_cbSendBuffer;
	SteamNetConnectionRealTimeStatus_t m_realtimeStatus;
	float m_flSendRate;
	float m_flRecvRate;
	int64 m_nSendInterval;
	int64 m_nRecvInterval;

	void Reset()
	{
		m_nReliableSendMsgCount = 0;
		m_nUnreliableSendMsgCount = 0;
		m_nReliableExpectedRecvMsg = 1;
		m_nExpectedRecvMsg = 1;
		m_flReliableMsgDelay = 0.0f;
		m_flUnreliableMsgDelay = 0.0f;
		m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
		m_bIsConnected = false;
		m_cbSendBuffer = 384 * 1024;
		memset( &m_realtimeStatus, 0, sizeof(m_realtimeStatus) );
		m_flSendRate = 0.0f;
		m_flRecvRate = 0.0f;
		m_nSendInterval = 0;
		m_nRecvInterval = 0;
	}

	void Close()
	{
		if ( m_hSteamNetConnection != k_HSteamNetConnection_Invalid )
		{
			SteamNetworkingSockets()->CloseConnection( m_hSteamNetConnection, 0, nullptr, false );
			m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
		}
		Reset();
	}

	inline void UpdateInterval( float flElapsed )
	{
		m_flSendRate = m_nSendInterval / flElapsed;
		m_flRecvRate = m_nRecvInterval / flElapsed;

		m_nSendInterval = 0;
		m_nRecvInterval = 0;
	}

	inline void UpdateStats()
	{
		SteamNetworkingSockets()->GetConnectionRealTimeStatus( m_hSteamNetConnection, &m_realtimeStatus, 0, nullptr );
	}

	void SetConnectionConfig()
	{
		SteamNetworkingUtils()->SetConnectionConfigValueInt32( m_hSteamNetConnection, k_ESteamNetworkingConfig_SendBufferSize, m_cbSendBuffer );
	}

	inline int GetQueuedSendBytes()
	{
		return m_realtimeStatus.m_cbPendingReliable + m_realtimeStatus.m_cbPendingUnreliable + m_realtimeStatus.m_cbSentUnackedReliable;
	}

	void SendRandomMessage( bool bReliable, int cbMaxSize )
	{
		TestMsg msg;
		msg.m_bReliable = bReliable;
		msg.m_usecWhenSent = SteamNetworkingUtils()->GetLocalTimestamp();
		msg.m_cbSize = std::uniform_int_distribution<>( 20, cbMaxSize )( g_rand );
		//bIsReliable = false;
		//nBytes = 1200-13;

		msg.m_nMsgNum = msg.m_bReliable ? ++m_nReliableSendMsgCount : ++m_nUnreliableSendMsgCount;
		for ( int n = 0; n < msg.m_cbSize; ++n )
		{
			msg.m_data[n] = (uint8)( msg.m_nMsgNum + n );
		}

		int cbSend = (int)( sizeof(msg) - sizeof(msg.m_data) + msg.m_cbSize );
		m_nSendInterval += cbSend;

		EResult result = SteamNetworkingSockets()->SendMessageToConnection(
			m_hSteamNetConnection,
			&msg,
			cbSend,
			msg.m_bReliable ? k_nSteamNetworkingSend_Reliable : k_nSteamNetworkingSend_Unreliable, nullptr );

		if ( result != k_EResultOK )
		{
			TEST_Printf( "***ERROR ON Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n",
				 m_sName.c_str(),
				 g_usecTestElapsed*1e-6,
				 msg.m_bReliable ? "reliable" : "unreliable",
				 (long long)msg.m_nMsgNum,
				 msg.m_cbSize,
				 GetQueuedSendBytes() );
			abort();
		}
	#if 0
		else
			TEST_Printf( "Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n",
				 connection.m_sName.c_str(),
				 g_usecTestElapsed*1e-6,
				 msg.m_bReliable ? "reliable" : "unreliable",
				 (long long)msg.m_nMsgNum,
				 msg.m_cbSize,
				 GetQueuedSendBytes() );
	#endif
	}

	void Send()
	{
		bool bReliable = std::uniform_real_distribution<>()( g_rand ) < .60;
		SendRandomMessage( bReliable, bReliable ? TestMsg::k_cbMaxSize : 2000 );
	}
};

static SFakePeer g_peerServer( "Server" );
static SFakePeer g_peerClient( "Client" );

static void CloseConnections()
{
	g_peerClient.Close();
	g_peerServer.Close();
	if ( g_hSteamListenSocket != k_HSteamNetConnection_Invalid )
	{
		SteamNetworkingSockets()->CloseListenSocket( g_hSteamListenSocket );
		g_hSteamListenSocket = k_HSteamNetConnection_Invalid;
	}
}

static void Recv( ISteamNetworkingSockets *pSteamSocketNetworking )
{

	while ( true )
	{
		SFakePeer *pConnection = &g_peerServer;
		ISteamNetworkingMessage *pIncomingMsg = nullptr;
		int numMsgs = pSteamSocketNetworking->ReceiveMessagesOnConnection( pConnection->m_hSteamNetConnection, &pIncomingMsg, 1 );
		if ( numMsgs <= 0 )
		{
			pConnection = &g_peerClient;
			numMsgs = pSteamSocketNetworking->ReceiveMessagesOnConnection( pConnection->m_hSteamNetConnection, &pIncomingMsg, 1 );
			if ( numMsgs <= 0 )
				return;
		}

		const TestMsg *pTestMsg = static_cast<const TestMsg*>( pIncomingMsg->GetData() );

		// Size makes sense?
		assert( sizeof(*pTestMsg) - sizeof(pTestMsg->m_data) + pTestMsg->m_cbSize == pIncomingMsg->GetSize() );

		// Check for sequence number anomaly.
		int64 &nExpectedMsgNum = pTestMsg->m_bReliable ? pConnection->m_nReliableExpectedRecvMsg : pConnection->m_nExpectedRecvMsg;
		if ( pTestMsg->m_nMsgNum != nExpectedMsgNum && pTestMsg->m_bReliable )
		{

			// Print that it happened.
			TEST_Printf(
				"Recv: %s, %s MISMATCH NUM wanted %lld got %lld\n",
				pConnection->m_sName.c_str(),
				pTestMsg->m_bReliable ? "RELIABLE" : "UNRELIABLE",
				(long long)nExpectedMsgNum,
				(long long)pTestMsg->m_nMsgNum );

			// This should not happen for reliable messages!
			assert( !pTestMsg->m_bReliable );
		}

		float flDelay = ( SteamNetworkingUtils()->GetLocalTimestamp() - pTestMsg->m_usecWhenSent ) * 1e-6f;
		pConnection->m_nRecvInterval += pIncomingMsg->GetSize();
		if ( pTestMsg->m_bReliable )
		{
			pConnection->m_flReliableMsgDelay += ( flDelay - pConnection->m_flReliableMsgDelay ) * .25f;
		}
		else
		{
			pConnection->m_flUnreliableMsgDelay += ( flDelay - pConnection->m_flUnreliableMsgDelay ) * .25f;
		}

		nExpectedMsgNum = pTestMsg->m_nMsgNum + 1;
		pIncomingMsg->Release();
	}
}

void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo )
{
	// What's the state of the connection?
	switch ( pInfo->m_info.m_eState )
	{
	case k_ESteamNetworkingConnectionState_ClosedByPeer:
	case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
		TEST_Printf( "Steam Net connection %x %s, reason %d: %s\n",
			pInfo->m_hConn,
			( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ? "closed by peer" : "problem detected locally" ),
			pInfo->m_info.m_eEndReason,
			pInfo->m_info.m_szEndDebug
		);

		// Close our end
		SteamNetworkingSockets()->CloseConnection( pInfo->m_hConn, 0, nullptr, false );

		if ( g_peerServer.m_hSteamNetConnection == pInfo->m_hConn )
		{
			g_peerServer.m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
		}
		if ( g_peerClient.m_hSteamNetConnection == pInfo->m_hConn )
		{
			g_peerClient.m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
		}

		break;

/*
	case k_ESteamNetworkingConnectionState_None:
		TEST_Printf( "No steam Net connection %x (%s)\n", pInfo->m_hConn, pInfo->m_info.m_steamIDRemote.Render() );

		if ( g_hSteamNetConnection == pInfo->m_hConn )
		{
			g_bIsConnected = false;
			g_hSteamNetConnection = k_HSteamNetConnection_Invalid;
		}
		break;
*/

	case k_ESteamNetworkingConnectionState_Connecting:

		// Is this a connection we initiated, or one that we are receiving?
		if ( g_hSteamListenSocket != k_HSteamListenSocket_Invalid && pInfo->m_info.m_hListenSocket == g_hSteamListenSocket )
		{
			// Somebody's knocking
			TEST_Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription );
			g_peerServer.m_hSteamNetConnection = pInfo->m_hConn;
			g_peerServer.m_bIsConnected = true;
			SteamNetworkingSockets()->AcceptConnection( pInfo->m_hConn );
			SteamNetworkingSockets()->SetConnectionName( g_peerServer.m_hSteamNetConnection, "Server" );
			g_peerServer.SetConnectionConfig();

		}
		break;

	case k_ESteamNetworkingConnectionState_Connected:
		if ( pInfo->m_hConn == g_peerClient.m_hSteamNetConnection )
		{
			g_peerClient.m_bIsConnected = true;
		}
		TEST_Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription );

		break;

	default:
		// Silences -Wswitch
		break;
	}
}

static void PumpCallbacksAndMakeSureStillConnected()
{
	TEST_PumpCallbacks();
	assert( g_peerClient.m_bIsConnected  );
	assert( g_peerServer.m_bIsConnected );
	assert( g_peerServer.m_hSteamNetConnection != k_HSteamNetConnection_Invalid );
	assert( g_peerClient.m_hSteamNetConnection != k_HSteamNetConnection_Invalid );
}

inline std::string FormatQuality( float q )
{
	if ( q < 0.0f ) return "???";
	char buf[32];
	sprintf( buf, "%.1f%%", q*100.0f );
	return buf;
}

static void PrintStatus( const SFakePeer &p1, const SFakePeer &p2 )
{
	const SteamNetConnectionRealTimeStatus_t &info1 = p1.m_realtimeStatus;
	const SteamNetConnectionRealTimeStatus_t &info2 = p2.m_realtimeStatus;
	TEST_Printf( "\n" );
	TEST_Printf( "%12s %12s\n", p1.m_sName.c_str(), p2.m_sName.c_str() );
	TEST_Printf( "%10dms %10dms  Ping\n", info1.m_nPing, info2.m_nPing );
	TEST_Printf( "%12s %12s  Quality\n", FormatQuality( info1.m_flConnectionQualityLocal ).c_str(), FormatQuality( info2.m_flConnectionQualityLocal ).c_str() );
	TEST_Printf( "%11.1fK %11.1fK  Send buffer\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )/1024.0f, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )/1024.0f );
	TEST_Printf( "%11.1fK %11.1fK  Send rate (app)\n", p1.m_flSendRate/1024.0f, p2.m_flSendRate/1024.0f );
	TEST_Printf( "%11.1fK %11.1fK  Send rate (wire)\n", info1.m_flOutBytesPerSec/1024.0f, info2.m_flOutBytesPerSec/1024.0f );
	TEST_Printf( "%12.1f %12.1f  Send pkts/sec (wire)\n", info1.m_flOutPacketsPerSec, info2.m_flOutPacketsPerSec );
	TEST_Printf( "%11.1fK %11.1fK  Send bandwidth (estimate)\n", info1.m_nSendRateBytesPerSecond/1024.0f, info2.m_nSendRateBytesPerSecond/1024.0f );
	TEST_Printf( "%11.1fK %11.1fK  Recv rate (app)\n", p1.m_flRecvRate/1024.0f, p2.m_flRecvRate/1024.0f );
	TEST_Printf( "%11.1fK %11.1fK  Recv rate (wire)\n", info1.m_flInBytesPerSec/1024.0f, info2.m_flInBytesPerSec/1024.0f );
	TEST_Printf( "%12.1f %12.1f  Recv pkts/sec (wire)\n", info1.m_flInPacketsPerSec, info2.m_flInPacketsPerSec );
	TEST_Printf( "%10.1fms %10.1fms  Send buffer drain time, based on bandwidth\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )*1000.0f/info1.m_nSendRateBytesPerSecond, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )*1000.0f/info2.m_nSendRateBytesPerSecond );
	TEST_Printf( "%10.1fms %10.1fms  App RTT (reliable)\n", p1.m_flReliableMsgDelay*1e3, p2.m_flReliableMsgDelay*1e3 );
	TEST_Printf( "%10.1fms %10.1fms  App RTT (unreliable)\n", p1.m_flUnreliableMsgDelay*1e3, p2.m_flUnreliableMsgDelay*1e3 );
}

static void ClearConfig()
{
	for (
		ESteamNetworkingConfigValue eValue = SteamNetworkingUtils()->IterateGenericEditableConfigValues( k_ESteamNetworkingConfig_Invalid, true );
		eValue != k_ESteamNetworkingConfig_Invalid;
		eValue = SteamNetworkingUtils()->IterateGenericEditableConfigValues( eValue, true )
	) {
		if ( eValue == k_ESteamNetworkingConfig_IP_AllowWithoutAuth )
			continue;
		SteamNetworkingUtils()->SetConfigValue( eValue, k_ESteamNetworkingConfig_Global, 0, k_ESteamNetworkingConfig_Int32, nullptr );
	}
}

static void TestNetworkConditions( int rate, float loss, int lag, float reorderPct, int reorderLag, bool bActLikeGame, ETestConnectionMode eMode )
{
	ISteamNetworkingSockets *pSteamSocketNetworking = SteamNetworkingSockets();

	TEST_Printf( "---------------------------------------------------\n" );
	TEST_Printf( "NETWORK CONDITIONS\n" );
	TEST_Printf( "Rate . . . . . . : %d Bps\n", rate );
	TEST_Printf( "Loss . . . . . . : %g%%\n", loss );
	TEST_Printf( "Ping . . . . . . : %d\n", lag*2 );
	TEST_Printf( "Reorder. . . . . : %g%% @ %dms\n", reorderPct, reorderLag );
	TEST_Printf( "Act like game. . : %d\n", (int)bActLikeGame );
	TEST_Printf( "---------------------------------------------------\n" );

	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_SendRateMin, rate );
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_SendRateMax, rate );

	SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Send, loss );
	SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Recv, 0 );
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Send, lag );
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Recv, 0 );
	SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketReorder_Send, reorderPct );
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketReorder_Time, reorderLag );

	SteamNetworkingMicroseconds usecWhenStarted = SteamNetworkingUtils()->GetLocalTimestamp();

	struct Timing { double flQuietSec; double flActiveSec; float flPrintIntervalSec; int nIterations; };
	static const Timing k_timing[] = {
		{ 0.5,  2.0, 1.0f, 1 }, // Cursory
		{ 1.0,  5.0, 2.0f, 2 }, // Normal
		{ 8.0, 25.0, 5.0f, 4 }, // Soak
	};
	const Timing &timing = k_timing[ (int)eMode ];

	SteamNetworkingMicroseconds usecQuietDuration  = SteamNetworkingMicroseconds( timing.flQuietSec  * 1e6 );
	SteamNetworkingMicroseconds usecActiveDuration = SteamNetworkingMicroseconds( timing.flActiveSec * 1e6 );
	float flWaitBetweenPrints = timing.flPrintIntervalSec;
	int nIterations = timing.nIterations;

	bool bQuiet = true;
	SteamNetworkingMicroseconds usecWhenStateEnd = 0;
	SteamNetworkingMicroseconds usecLastPrint = SteamNetworkingUtils()->GetLocalTimestamp();

	while ( true )
	{
		SteamNetworkingMicroseconds now = SteamNetworkingUtils()->GetLocalTimestamp();
		g_usecTestElapsed = now - usecWhenStarted;
		// If flElapsed > 1.0 when in debug, just slamp it
		//if ( Plat_IsInDebugSession() && now - flLastNow > 1.0f )
		//{
		//	flStartTime += now - flLastNow - 1.0f;
		//	flElapsed = now - flStartTime;
		//}
		//usecLastNow = now;

		g_peerServer.UpdateStats();
		g_peerClient.UpdateStats();

		int nServerPending = g_peerServer.GetQueuedSendBytes();
		int nClientPending = g_peerClient.GetQueuedSendBytes();

		bool bCheckStateChange = ( usecWhenStateEnd == 0 );
		float flElapsedPrint = ( now - usecLastPrint ) * 1e-6f;
		if ( flElapsedPrint > flWaitBetweenPrints )
		{
			g_peerServer.UpdateInterval( flElapsedPrint );
			g_peerClient.UpdateInterval( flElapsedPrint );
			PrintStatus( g_peerServer, g_peerClient );
			usecLastPrint = now;
			bCheckStateChange = true;
		}

		// Start and stop active use of the connection.  This exercises a bunch of important edge cases
		// such as keepalives, the bandwidth estimation, etc.
		if ( bCheckStateChange && g_usecTestElapsed > usecWhenStateEnd )
		{
			if ( bQuiet )
			{
				if ( nServerPending == 0 &&  nClientPending == 0 )
				{
					bQuiet = false;
					usecWhenStateEnd = g_usecTestElapsed + ( bQuiet ? usecQuietDuration : usecActiveDuration );
					if ( nIterations-- <= 0 )
						break;
					TEST_Printf( "Entering active time (sending enabled)\n" );
				}
			}
			else
			{
				bQuiet = true;
				usecWhenStateEnd = g_usecTestElapsed + usecQuietDuration;
				TEST_Printf( "Entering quiet time (no sending) to see how fast queues drain\n" );
			}
		}

		if ( !bQuiet )
		{
			if ( nServerPending < g_peerServer.m_cbSendBuffer - 16*1024 )
			{
				if ( bActLikeGame )
				{
					g_peerServer.SendRandomMessage( true, 4000 );
					g_peerServer.SendRandomMessage( false, 2000 );
				}
				else
				{
					g_peerServer.Send();
				}
			}
			if ( nClientPending < g_peerClient.m_cbSendBuffer - 16*1024 )
			{
				if ( bActLikeGame )
				{
					g_peerClient.SendRandomMessage( true, 4000 );
					g_peerClient.SendRandomMessage( false, 2000 );
				}
				else
				{
					g_peerClient.Send();
				}
			}
		}
		PumpCallbacksAndMakeSureStillConnected();
		Recv( pSteamSocketNetworking );
		if ( bActLikeGame )
			std::this_thread::sleep_for( std::chrono::milliseconds( 30 ) );
	}
}

static void Test_Connection( ETestConnectionMode eMode, const SteamNetworkingIPAddr &addrServerBind, const SteamNetworkingIPAddr &addrClientConnect )
{
	static const char *const k_rgszModeName[] = { "Cursory", "Normal", "Soak" };
	TEST_Printf( "***************************************************\n" );
	TEST_Printf( "Mode:           %s\n", k_rgszModeName[ (int)eMode ] );
	TEST_Printf( "Server bind:    %s\n", SteamNetworkingIPAddrRender( addrServerBind ).c_str() );
	TEST_Printf( "Client connect: %s\n", SteamNetworkingIPAddrRender( addrClientConnect ).c_str() );
	TEST_Printf( "***************************************************\n" );

	SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged );

	CloseConnections();

	ISteamNetworkingSockets *pSteamSocketNetworking = SteamNetworkingSockets();

	// Initiate connection
	g_hSteamListenSocket = pSteamSocketNetworking->CreateListenSocketIP( addrServerBind, 0, nullptr );
	g_peerClient.m_hSteamNetConnection = pSteamSocketNetworking->ConnectByIPAddress( addrClientConnect, 0, nullptr );
	pSteamSocketNetworking->SetConnectionName( g_peerClient.m_hSteamNetConnection, "Client" );

	g_peerClient.SetConnectionConfig();

//	// Send a few random message, before we get connected, just to test that case
//	g_peerClient.SendRandomMessage( true );
//	g_peerClient.SendRandomMessage( true );
//	g_peerClient.SendRandomMessage( true );

	// Wait for connection to complete
	while ( !g_peerClient.m_bIsConnected || !g_peerServer.m_bIsConnected )
		TEST_PumpCallbacks();

	auto Test = [eMode]( int rate, float loss, int lag, float reorderPct, int reorderLag )
	{
		TestNetworkConditions( rate, loss, lag, reorderPct, reorderLag, false, eMode );
		TestNetworkConditions( rate, loss, lag, reorderPct, reorderLag, true, eMode );
	};

	if ( eMode == ETestConnectionMode::Cursory )
	{
		Test(  128000, 10, 50, 2, 50 ); // Low bandwidth, high packet loss
	}
	else if ( eMode == ETestConnectionMode::Normal )
	{
		Test(  128000, 10, 50, 2, 50 ); // Low bandwidth, high packet loss
		Test( 1000000,  5, 10, 1, 10 ); // Medium bandwidth, still pretty bad packet loss
	}
	else
	{
		Test( 64000, 20, 100, 4, 50 ); // low bandwidth, terrible packet loss
		Test( 1000000, 20, 100, 4, 10 ); // high bandwidth, terrible packet loss
		Test( 1000000, 2, 5, 2, 1 ); // wifi (high bandwidth, low packet loss, occasional reordering with very small delay)
		Test( 2000000, 0, 0, 0, 0 ); // LAN (high bandwidth, negligible lag/loss)
		Test( 128000, 20, 100, 4, 40 );
		Test( 500000, 20, 100, 4, 30 );

		Test( 64000, 0, 0, 0, 0 );
		Test( 128000, 0, 0, 0, 0 );
		Test( 256000, 0, 0, 0, 0 );
		Test( 500000, 0, 0, 0, 0 );
		Test( 1000000, 0, 0, 0, 0 );

		Test( 64000, 1, 25, 1, 10 );
		Test( 1000000, 1, 25, 1, 10 );

		Test( 64000, 5, 50, 2, 50 );
		Test( 1000000, 5, 50, 2, 10 );
	}

	CloseConnections();
}

const int k_nStartingServerPort = 27200;

static void Test_quick()
{
	uint16 nServerPort = k_nStartingServerPort;
	SteamNetworkingIPAddr bindAddr, connectAddr;

	//
	// First, do some 'cursory' connection tests between various combiantions of IPv4, IPc6, and dual-stack.
	// This is really just to make sure we can connect and exchange packets.
	//

	// IPv4-only server, IPv4 client
	bindAddr.SetIPv4( 0, nServerPort );
	connectAddr.SetIPv4( 0x7f000001, nServerPort );
	Test_Connection( ETestConnectionMode::Cursory, bindAddr, connectAddr );
	++nServerPort;

	// IPv6-only server, IPv6 client
	bindAddr.SetIPv6LocalHost( nServerPort );
	connectAddr.SetIPv6LocalHost( nServerPort );
	Test_Connection( ETestConnectionMode::Cursory, bindAddr, connectAddr );
	++nServerPort;

	// Dual-stack server, IPv6 client
	bindAddr.Clear(); bindAddr.m_port = nServerPort;
	connectAddr.SetIPv6LocalHost( nServerPort );
	Test_Connection( ETestConnectionMode::Cursory, bindAddr, connectAddr );
	++nServerPort;

	//
	// Now do a 'normal' test
	//
	// Dual-stack server, IPv4 client (IPv4-mapped path)
	bindAddr.Clear(); bindAddr.m_port = nServerPort;
	connectAddr.SetIPv4( 0x7f000001, nServerPort );
	Test_Connection( ETestConnectionMode::Normal, bindAddr, connectAddr );
}

static void Test_soak()
{
	uint16 nServerPort = k_nStartingServerPort;
	SteamNetworkingIPAddr bindAddr, connectAddr;

	// Dual-stack server, IPv4 client (IPv4-mapped path)
	bindAddr.Clear(); bindAddr.m_port = nServerPort;
	connectAddr.SetIPv4( 0x7f000001, nServerPort );
	Test_Connection( ETestConnectionMode::Soak, bindAddr, connectAddr );
}

// Some tests for identity string handling.  Doesn't really have anything to do with
// connectivity, this is just a conveinent place for this to live
void Test_identity()
{
	SteamNetworkingIdentity id1, id2;
	char tempBuf[ SteamNetworkingIdentity::k_cchMaxString ];

	{
		CSteamID steamID( 1234, k_EUniversePublic, k_EAccountTypeIndividual );
		id1.SetSteamID( steamID );
		id1.ToString( tempBuf, sizeof(tempBuf ) );
		assert( id2.ParseString( tempBuf ) );
		assert( id2.GetSteamID() == steamID );
	}

	{
		const char *pszTempXBoxID = "8fg37rfsdf";
		assert( id1.SetXboxPairwiseID( pszTempXBoxID ) );
		id1.ToString( tempBuf, sizeof(tempBuf ) );
		assert( id2.ParseString( tempBuf ) );
		assert( strcmp( id2.GetXboxPairwiseID(), pszTempXBoxID ) == 0 );
	}

	{
		const char *pszTempIPAddr = "ip:192.168.0.0:27015";
		assert( id1.ParseString( pszTempIPAddr ) );
		id1.ToString( tempBuf, sizeof(tempBuf ) );
		assert( strcmp( tempBuf, pszTempIPAddr ) == 0 );

		id1.SetLocalHost();
		id1.ToString( tempBuf, sizeof(tempBuf ) );
		assert( strcmp( tempBuf, "ip:::1" ) == 0 );
	}

	{
		const char *pszTempGenStr = "Locke Lamora";
		assert( id1.SetGenericString( pszTempGenStr ) );
		id1.ToString( tempBuf, sizeof(tempBuf ) );
		assert( strcmp( tempBuf, "str:Locke Lamora" ) == 0 );
		assert( id2.ParseString( tempBuf ) );
		assert( strcmp( id2.GetGenericString(), pszTempGenStr ) == 0 );
	}
}

void Test_lane_quick_queueanddrain()
{
	// Create a loopback connection, over the local network.
	// (With a loopback over internal buffers, all messages
	// are delivered instantly and lanes are irrelevant.)
	HSteamNetConnection hSender, hRecver;
	SteamNetworkingIdentity identSender, identRecver;
	identSender.SetGenericString( "sender" );
	identRecver.SetGenericString( "receiver" );
	// NOTE: each pPeerIdentity argument is the remote identity observed by the
	// *corresponding* connection, so pass them swapped relative to the connection handles.
	assert( SteamNetworkingSockets()->CreateSocketPair( &hSender, &hRecver, true, &identRecver, &identSender ) );

	// Verify: hSender sees "receiver" as its remote peer, hRecver sees "sender".
	{
		SteamNetConnectionInfo_t connInfoSender, connInfoRecver;
		assert( SteamNetworkingSockets()->GetConnectionInfo( hSender, &connInfoSender ) );
		assert( SteamNetworkingSockets()->GetConnectionInfo( hRecver, &connInfoRecver ) );
		assert( connInfoSender.m_identityRemote == identRecver );
		assert( connInfoRecver.m_identityRemote == identSender );
	}

	// Set the send rate to a fixed value
	const int k_nSendRate = 128*1024;
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendRateMin, k_nSendRate );
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendRateMax, k_nSendRate );

	// Configure lanes.
	constexpr int k_nLanes = 4;
	int priorities[k_nLanes] = { 2,  0,  1,  1 };
	uint16 weights[k_nLanes] = { 1,  1, 25, 75 };
	assert( k_EResultOK == SteamNetworkingSockets()->ConfigureConnectionLanes( hSender, k_nLanes, priorities, weights ) );

	// We're gonna dump a whole bunch of stuff at once and watch it
	// drain, broke up into messages
	constexpr int k_nMsgPerLane = 128;
	constexpr int k_cbMsg = 1024; // We're sending unreliable messages, so don't make them huge
	constexpr int k_cbLaneData = k_nMsgPerLane * k_cbMsg;
	constexpr int k_nTotalMsg = k_nLanes * k_nMsgPerLane;
	constexpr int k_cbTotalData = k_nLanes * k_cbLaneData;

	// Allow us to buffer up a whole bunch
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendBufferSize, k_cbTotalData + 1024 );

	// Dump a fixed amount of data into each lane
	{
		SteamNetworkingMessage_t *pMessages[k_nTotalMsg];
		int idxMsg = 0;
		for ( int idxLane = 0 ; idxLane < k_nLanes ; ++idxLane )
		{
			for ( int j = 0 ; j < k_nMsgPerLane ; ++j )
			{
				 SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( k_cbMsg );
				 assert( pMsg->m_cbSize == k_cbMsg );
				 pMsg->m_conn = hSender;
				 pMsg->m_nFlags = 0; // Just send everything unreliable for this test
				 pMsg->m_idxLane = (uint16)idxLane;
				 pMessages[idxMsg++] = pMsg;
			}
		}
		assert( idxMsg == k_nTotalMsg );
		SteamNetworkingSockets()->SendMessages( idxMsg, pMessages, nullptr, true );
	}

	// Remember when we sent all the messages
	SteamNetworkingMicroseconds usecStartTime = SteamNetworkingUtils()->GetLocalTimestamp();

	// Get back realtime status
	SteamNetConnectionRealTimeStatus_t status;
	SteamNetConnectionRealTimeLaneStatus_t laneStatus[k_nLanes];
	assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hSender, &status, k_nLanes, laneStatus ) );

	// We should be able to predict the results of these estimates very accurately
	const SteamNetworkingMicroseconds usecTol = 50*1000;
	const double flSecTol = usecTol * 1e-6;
	const int cbTol = (int)( k_nSendRate * flSecTol );

	assert( status.m_cbPendingReliable == 0 );
	assert( status.m_cbPendingUnreliable <= k_cbTotalData );
	assert( status.m_cbPendingUnreliable > k_cbTotalData - cbTol );

	// Lane 1 has the lowest priority number, and should be the only one
	// with any data sent
	assert( laneStatus[1].m_cbPendingReliable == 0 );
	assert( laneStatus[1].m_cbPendingUnreliable <= k_cbLaneData );
	assert( laneStatus[1].m_cbPendingUnreliable > k_cbLaneData - cbTol );
	SteamNetworkingMicroseconds usecExpectedQueueTime1 = (SteamNetworkingMicroseconds)( laneStatus[1].m_cbPendingUnreliable * 1e6 / k_nSendRate );
	assert( laneStatus[1].m_usecQueueTime < usecExpectedQueueTime1 + usecTol );
	assert( laneStatus[1].m_usecQueueTime > usecExpectedQueueTime1 - usecTol );

	// After lane 1 finishes, we expect the next chunk of time
	// to be divided between lanes 2 and 3, at a ratio of 25:75, respectively.
	// This means when lane 3 finishes, we will have sent all of lane 3,
	// plus 1/3rd of lane 2.  Thus it will finish in the time it takes
	// to send 4/3rd of a lane.
	assert( laneStatus[3].m_cbPendingReliable == 0 );
	assert( laneStatus[3].m_cbPendingUnreliable == k_cbLaneData ); // Should not have sent any data on 3 yet
	SteamNetworkingMicroseconds usecExpectedQueueTime3 = (SteamNetworkingMicroseconds)( usecExpectedQueueTime1 + k_cbLaneData * 4.0/3.0 * 1e6 / k_nSendRate );
	assert( laneStatus[3].m_usecQueueTime < usecExpectedQueueTime3 + usecTol );
	assert( laneStatus[3].m_usecQueueTime > usecExpectedQueueTime3 - usecTol );

	// After lane 3 finishes, lane 2 will have the connection all to itself,
	// to send the remaining 2/3rd of the complete lane data we buffered
	assert( laneStatus[2].m_cbPendingReliable == 0 );
	assert( laneStatus[2].m_cbPendingUnreliable == k_cbLaneData ); // Should not have sent any data on 2 yet
	SteamNetworkingMicroseconds usecExpectedQueueTime2 = (SteamNetworkingMicroseconds)( usecExpectedQueueTime3 + k_cbLaneData * 2.0/3.0 * 1e6 / k_nSendRate );
	assert( laneStatus[2].m_usecQueueTime < usecExpectedQueueTime2 + usecTol );
	assert( laneStatus[2].m_usecQueueTime > usecExpectedQueueTime2 - usecTol );

	// Finally, lane 0, the lowest priority lane, will drain
	assert( laneStatus[0].m_cbPendingReliable == 0 );
	assert( laneStatus[0].m_cbPendingUnreliable == k_cbLaneData ); // Should not have sent any data on 0 yet
	SteamNetworkingMicroseconds usecExpectedQueueTime0 = (SteamNetworkingMicroseconds)( usecExpectedQueueTime2 + k_cbLaneData * 1e6 / k_nSendRate );
	assert( laneStatus[0].m_usecQueueTime < usecExpectedQueueTime0 + usecTol );
	assert( laneStatus[0].m_usecQueueTime > usecExpectedQueueTime0 - usecTol );

	// Send one last one-byte message on each lane,
	// so we can tell when we are done for that lane
	{
		SteamNetworkingMessage_t *pMessages[k_nLanes];
		for ( int idxLane = 0 ; idxLane < k_nLanes ; ++idxLane )
		{
			SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( 1 );
			assert( pMsg->m_cbSize == 1 );
			pMsg->m_conn = hSender;
			pMsg->m_nFlags = 0; // Just send everything unreliable for this test
			pMsg->m_idxLane = (uint16)idxLane;
			pMessages[idxLane] = pMsg;
		}
		SteamNetworkingSockets()->SendMessages( k_nLanes, pMessages, nullptr, true );
	}

	int cbLaneReceived[k_nLanes] = {};
	int nLanesFinished = 0;
	while ( nLanesFinished < k_nLanes )
	{
		SteamNetworkingMessage_t *pMsg;
		while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hRecver, &pMsg, 1 ) == 1 )
		{
			int idxLane = pMsg->m_idxLane;

			//TEST_Printf( "RX Lane %d msg %lld sz %d\n", idxLane, pMsg->m_nMessageNumber, pMsg->m_cbSize );

			switch ( idxLane )
			{
				case 1:
					assert( cbLaneReceived[2] == 0 );
					assert( cbLaneReceived[3] == 0 );
					assert( cbLaneReceived[0] == 0 );
					break;
				case 3:
					assert( cbLaneReceived[1] == k_cbLaneData+1 );
					assert( nLanesFinished == 1 );
					assert( cbLaneReceived[0] == 0 );
					break;
				case 2:
					assert( cbLaneReceived[1] == k_cbLaneData+1 );
					assert( nLanesFinished == 1 || nLanesFinished == 2 );
					assert( cbLaneReceived[0] < 2048 ); // First bit of lane 0 might come in before the last part of this lane
					break;
				case 0:
					assert( cbLaneReceived[1] == k_cbLaneData+1 );
					assert( cbLaneReceived[3] == k_cbLaneData+1 );

					// The very first packet might deliver some data on lane 0
					// before the other lanes due to internal quirks of serialization,
					// but in general we should have finished all the other lanes first
					if ( cbLaneReceived[0] > 2048 )
					{
						assert( cbLaneReceived[2] == k_cbLaneData+1 );
						assert( nLanesFinished == 3 );
					}
					break;
			}
			cbLaneReceived[idxLane] += pMsg->m_cbSize;
			if ( pMsg->m_cbSize == 1 )
			{
				assert( cbLaneReceived[idxLane] == k_cbLaneData+1 );
				++nLanesFinished;
				float msElapsed = ( SteamNetworkingUtils()->GetLocalTimestamp() - usecStartTime ) * 1e-3f;
				TEST_Printf( "Lane %d finished @ %.1fms, expected %.1fms.  %6d %6d %6d %6d\n",
					idxLane,
					msElapsed, laneStatus[idxLane].m_usecQueueTime * 1e-3f,
					cbLaneReceived[0],
					cbLaneReceived[1],
					cbLaneReceived[2],
					cbLaneReceived[3]
				);

				if ( idxLane == 3 )
				{
					assert( cbLaneReceived[2] * 75 <= ( cbLaneReceived[3]+k_cbMsg ) * 25 );
					assert( (cbLaneReceived[2]+k_cbMsg) * 75 >= cbLaneReceived[3] * 25 );
				}
			}
			else
			{
				assert( pMsg->m_cbSize == k_cbMsg );
				assert( cbLaneReceived[idxLane] <= k_cbLaneData );
			}
			pMsg->Release();
		}
		TEST_PumpCallbacks();
	}

	// Cleanup
	SteamNetworkingSockets()->CloseConnection( hSender, 0, nullptr, false );
	SteamNetworkingSockets()->CloseConnection( hRecver, 0, nullptr, false );
}

// Test a particular use case that we have specifically been asked about:
// Three lanes:
// - Lane for most gameplay traffic.
// - "Priority" lane for certain urgent messages
// - "Background" land for content download
void Test_lane_quick_priority_and_background()
{
	// Create a loopback connection, over the local network.
	// (With a loopback over internal buffers, all messages
	// are delivered instantly and lanes are irrelevant.)
	HSteamNetConnection hServer, hClient;
	SteamNetworkingIdentity identServer, identClient;
	identServer.SetGenericString( "server" );
	identClient.SetGenericString( "client" );
	// NOTE: each pPeerIdentity argument is the remote identity observed by the
	// *corresponding* connection, so pass them swapped relative to the connection handles.
	assert( SteamNetworkingSockets()->CreateSocketPair( &hServer, &hClient, true, &identClient, &identServer ) );
	SteamNetworkingSockets()->SetConnectionName( hServer, "server" );
	SteamNetworkingSockets()->SetConnectionName( hClient, "client" );

	// Verify: hServer sees "client" as its remote peer, hClient sees "server".
	{
		SteamNetConnectionInfo_t connInfoServer, connInfoClient;
		assert( SteamNetworkingSockets()->GetConnectionInfo( hServer, &connInfoServer ) );
		assert( SteamNetworkingSockets()->GetConnectionInfo( hClient, &connInfoClient ) );
		assert( connInfoServer.m_identityRemote == identClient );
		assert( connInfoClient.m_identityRemote == identServer );
	}

	// Set the send rate to a fixed value
	const int k_nSendRate = 256*1024;
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMin, k_nSendRate );
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMax, k_nSendRate );

	// Configure lanes.
	constexpr int k_nLanes = 3;
	constexpr int k_LaneGameplay = 0;
	constexpr int k_LaneUrgent = 1;
	constexpr int k_LaneBackground = 2;
	int priorities[k_nLanes] = {  1, 0,  1 };
	uint16 weights[k_nLanes] = { 75, 1, 25 };
	assert( k_EResultOK == SteamNetworkingSockets()->ConfigureConnectionLanes( hServer, k_nLanes, priorities, weights ) );

	// Allow us to buffer up a decent amount for background content download
	constexpr int k_cbMaxBackgroundInFlight = 1024 * 1024;
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendBufferSize, k_cbMaxBackgroundInFlight + 64*1024 );

	// Remember when we sent all the messages
	SteamNetworkingMicroseconds usecStartTime = SteamNetworkingUtils()->GetLocalTimestamp();
	SteamNetworkingMicroseconds usecNextSendUrgent = 0;
	SteamNetworkingMicroseconds usecNextSendGameplay = usecStartTime;

	const int k_nFakeLag = 50;

	// Set some reasonable network conditions
	SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Send, 2.0 );
	SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Recv, 0 );
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Send, k_nFakeLag );
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Recv, 0 );
	SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketReorder_Send, .5 );
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketReorder_Time, 25 );

	int64 nMsgSent[ k_nLanes ] = {};
	int64 nMsgRecv[ k_nLanes ] = {};
	int64 nLatencyTotal[ k_nLanes ] = {};
	int64 nLatencySqTotal[ k_nLanes ] = {};

	for (;;)
	{
		SteamNetworkingMicroseconds usecNow = SteamNetworkingUtils()->GetLocalTimestamp();

		// RUn the test for 60 seconds
		if ( usecStartTime + 60*1000*1000 < usecNow )
			break;

		// Get back realtime status
		SteamNetConnectionRealTimeStatus_t status;
		SteamNetConnectionRealTimeLaneStatus_t laneStatus[k_nLanes];
		assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hServer, &status, k_nLanes, laneStatus ) );

		// Keep the background download pipe full
		if ( laneStatus[ k_LaneBackground ].m_cbPendingReliable + k_cbMaxSteamNetworkingSocketsMessageSizeSend <= k_cbMaxBackgroundInFlight )
		{
			SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( k_cbMaxSteamNetworkingSocketsMessageSizeSend );
			pMsg->m_conn = hServer;
			pMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;
			pMsg->m_idxLane = k_LaneBackground;

			// Shove the time when we sent it into the body,
			// but otherwise leave the rest of the body unitialized
			*(SteamNetworkingMicroseconds *)pMsg->m_pData = usecNow;

			int64 nMsgNum = 0;
			SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum, true );
			++nMsgSent[k_LaneBackground];
			assert( nMsgNum == nMsgSent[k_LaneBackground] );
		}

		// Time to send a small priority message?
		if ( usecNow >= usecNextSendUrgent )
		{
			SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( std::uniform_int_distribution<>( 100, 500 )( g_rand ) );
			pMsg->m_conn = hServer;
			pMsg->m_nFlags = k_nSteamNetworkingSend_ReliableNoNagle;
			pMsg->m_idxLane = k_LaneUrgent;

			// Shove the time when we sent it into the body,
			// but otherwise leave the rest of the body unitialized
			*(SteamNetworkingMicroseconds *)pMsg->m_pData = usecNow;

			int64 nMsgNum = 0;
			SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum, true );
			++nMsgSent[k_LaneUrgent];
			assert( nMsgNum == nMsgSent[k_LaneUrgent] );

			// Schedule the next send at a random interval
			usecNextSendUrgent = usecNow + std::uniform_int_distribution<>( 500, 1500 )( g_rand ) * 1000;
		}

		// Time to send a gameplay message
		if ( usecNow >= usecNextSendGameplay )
		{

			// Send a gameplay message server->client
			{
				SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( std::uniform_int_distribution<>( 1000, 5000 )( g_rand ) );
				pMsg->m_conn = hServer;
				pMsg->m_idxLane = k_LaneGameplay;

				// Occasionally send reliable
				pMsg->m_nFlags = std::uniform_int_distribution<>( 0, 100 )( g_rand ) < 30 ? k_nSteamNetworkingSend_ReliableNoNagle : k_nSteamNetworkingSend_UnreliableNoNagle;

				// Shove the time when we sent it into the body,
				// but otherwise leave the rest of the body unitialized
				*(SteamNetworkingMicroseconds *)pMsg->m_pData = usecNow;

				int64 nMsgNum = 0;
				SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum, true );
				++nMsgSent[k_LaneGameplay];
				assert( nMsgNum == nMsgSent[k_LaneGameplay] );
			}

			// Send a gameplay message client->server
			{
				SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( std::uniform_int_distribution<>( 100, 2000 )( g_rand ) );
				pMsg->m_conn = hClient;
				pMsg->m_idxLane = k_LaneGameplay;

				// Occasionally send reliable
				pMsg->m_nFlags = std::uniform_int_distribution<>( 0, 100 )( g_rand ) < 30 ? k_nSteamNetworkingSend_ReliableNoNagle : k_nSteamNetworkingSend_UnreliableNoNagle;

				int64 nMsgNum = 0;
				SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum, true );
				assert( nMsgNum > 0 );
			}

			// Schedule the next send at 30hz
			usecNextSendGameplay += 1000*1000 / 30;
		}

		usecNow = SteamNetworkingUtils()->GetLocalTimestamp();

		// Client receive messages
		{
			SteamNetworkingMessage_t *pMsg;
			while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hClient, &pMsg, 1 ) == 1 )
			{
				SteamNetworkingMicroseconds usecLatency = usecNow - *(SteamNetworkingMicroseconds*)pMsg->m_pData;
				int idxLane = pMsg->m_idxLane;
				if ( idxLane != k_LaneGameplay || pMsg->m_nMessageNumber%30 == 0 )
					TEST_Printf( "RX lane %d one-way latency %6.1fms  #%lld\n", idxLane, usecLatency*1e-3, pMsg->m_nMessageNumber );

				++nMsgRecv[ idxLane ];
				if ( idxLane != k_LaneGameplay )
					assert( pMsg->m_nMessageNumber == nMsgRecv[ idxLane ] );

				int msLatency = (int)( usecLatency / 1000 );
				nLatencyTotal[ idxLane ] += msLatency;
				nLatencySqTotal[ idxLane ] += msLatency*msLatency;

				pMsg->Release();
			}
		}

		// Server receive messages
		{
			SteamNetworkingMessage_t *pMsg;
			while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hServer, &pMsg, 1 ) == 1 )
			{
				pMsg->Release();
			}
		}

		// Background tasks, etc
		TEST_PumpCallbacks();
	}
	TEST_Printf( "\n\n" );

	// Cleanup
	SteamNetworkingSockets()->CloseConnection( hServer, 0, nullptr, false );
	SteamNetworkingSockets()->CloseConnection( hClient, 0, nullptr, false );

	float flAvgLatencyMS[ k_nLanes ];
	float flRMSLatencyMS[ k_nLanes ];
	for ( int i = 0 ; i < 3 ; ++i )
	{
		flAvgLatencyMS[i] = (float)nLatencyTotal[i] / (float)nMsgRecv[i];
		flRMSLatencyMS[i] = sqrt( (float)nLatencySqTotal[i] / (float)nMsgRecv[i] );

		// FIXME - print stddev?
		TEST_Printf( "Lane %d: %6lld msgs, one-way latency avg %6.1fms, RMS %6.1fms\n",
			i, nMsgRecv[i], flAvgLatencyMS[i], flRMSLatencyMS[i] );
	}

	// Check numbers...PIOMA
	//assert( flRMSPingMS[k_LaneUrgent] < flRMSPingMS[k_LaneGameplay] ); // We are comparing pings of always reliable msgs to pings that might sometimes be unreliable, so not totally a fair comparison
	//assert( flRMSPingMS[k_LaneGameplay] < k_nFakeLag*1.2 + 10 );

}

void Test_pipe()
{
	TEST_Printf( "***************************************************\n" );
	TEST_Printf( "Pipe (socket pair, no network loopback)\n" );
	TEST_Printf( "***************************************************\n" );

	// CreateSocketPair with bUseNetworkLoopback=false: pure internal buffer path,
	// no encryption, no packet fragmentation, no network stack involvement.
	HSteamNetConnection hAlice, hBob;
	SteamNetworkingIdentity identAlice, identBob;
	identAlice.SetGenericString( "alice" );
	identBob.SetGenericString( "bob" );
	// NOTE: each pPeerIdentity is the remote identity seen by the *corresponding* connection.
	assert( SteamNetworkingSockets()->CreateSocketPair( &hAlice, &hBob, false, &identBob, &identAlice ) );

	// Verify each end observes the correct remote identity.
	{
		SteamNetConnectionInfo_t infoAlice, infoBob;
		assert( SteamNetworkingSockets()->GetConnectionInfo( hAlice, &infoAlice ) );
		assert( SteamNetworkingSockets()->GetConnectionInfo( hBob, &infoBob ) );
		assert( infoAlice.m_identityRemote == identBob );
		assert( infoBob.m_identityRemote == identAlice );
	}

	// Wire up to the global peer state used by TestNetworkConditions / PumpCallbacksAndMakeSureStillConnected.
	g_peerClient.Reset();
	g_peerServer.Reset();
	g_peerClient.m_hSteamNetConnection = hAlice;
	g_peerClient.m_bIsConnected = true;
	g_peerClient.SetConnectionConfig();
	g_peerServer.m_hSteamNetConnection = hBob;
	g_peerServer.m_bIsConnected = true;
	g_peerServer.SetConnectionConfig();

	// Cursory connection test. Fake loss/lag config has no effect on a pipe
	// (no network path), but the send/receive loop still exercises the connection.
	TestNetworkConditions( 10*1000*1000, 0, 0, 0, 0, false, ETestConnectionMode::Cursory );

	// Zero-copy: for a pipe connection the received message must point at the
	// exact same data buffer that was handed to SendMessages -- no copy through
	// any network or encryption layer.
	{
		SteamNetworkingMessage_t *pSendMsg = SteamNetworkingUtils()->AllocateMessage( 256 );
		pSendMsg->m_conn = hAlice;
		pSendMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;
		void *pSendData = pSendMsg->m_pData;

		int64 nMsgNum = 0;
		SteamNetworkingSockets()->SendMessages( 1, &pSendMsg, &nMsgNum, true );
		assert( nMsgNum > 0 );

		SteamNetworkingMessage_t *pRecvMsg = nullptr;
		int nRecv = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hBob, &pRecvMsg, 1 );
		assert( nRecv == 1 );
		assert( pRecvMsg->m_pData == pSendData ); // zero-copy: must be the same pointer
		pRecvMsg->Release();
	}

	// Oversized messages: pipe connections have no network-imposed size limit, so
	// messages larger than k_cbMaxSteamNetworkingSocketsMessageSizeSend must work.
	{
		const int k_cbHuge = k_cbMaxSteamNetworkingSocketsMessageSizeSend * 3;
		SteamNetworkingMessage_t *pSendMsg = SteamNetworkingUtils()->AllocateMessage( k_cbHuge );
		pSendMsg->m_conn = hAlice;
		pSendMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;

		int64 nMsgNum = 0;
		SteamNetworkingSockets()->SendMessages( 1, &pSendMsg, &nMsgNum, true );
		assert( nMsgNum > 0 );

		SteamNetworkingMessage_t *pRecvMsg = nullptr;
		int nRecv = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hBob, &pRecvMsg, 1 );
		assert( nRecv == 1 );
		assert( pRecvMsg->m_cbSize == k_cbHuge );
		pRecvMsg->Release();
	}

	SteamNetworkingSockets()->CloseConnection( hAlice, 0, nullptr, false );
	SteamNetworkingSockets()->CloseConnection( hBob, 0, nullptr, false );
	g_peerClient.Reset();
	g_peerServer.Reset();
}

void Test_netloopback_throughput()
{
	// Create a loopback connection, over the local network.
	HSteamNetConnection hServer, hClient;
	assert( SteamNetworkingSockets()->CreateSocketPair( &hServer, &hClient, true, nullptr, nullptr ) );
	SteamNetworkingSockets()->SetConnectionName( hServer, "server" );
	SteamNetworkingSockets()->SetConnectionName( hClient, "client" );

	// Try several increasing send rates and make sure we can keep up
	// FIXME Something broken here with this test above 30000, that isn't reproducing
	// for me locally.  Temporarily removing the higher rates until I can investigate.
	//for ( int nSendRateKB: { 8000, 12000, 16000, 20000, 30000, 40000, 50000, 60000 } )
	for ( int nSendRateKB: { 8000, 12000, 16000, 20000, 30000 } )
	{
		const int nSendRate = nSendRateKB*1000; // Use powers of 10 here, not 1024

		TEST_Printf( "-- TESTING SEND RATE: %dKB/sec -------\n\n", nSendRateKB );

		// Set the send rate to a fixed value
		SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMin, nSendRate );
		SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMax, nSendRate );
		SteamNetworkingUtils()->SetConnectionConfigValueInt32( hClient, k_ESteamNetworkingConfig_SendRateMin, nSendRate );
		SteamNetworkingUtils()->SetConnectionConfigValueInt32( hClient, k_ESteamNetworkingConfig_SendRateMax, nSendRate );
		SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_LogLevel_PacketGaps, k_ESteamNetworkingSocketsDebugOutputType_Verbose );

		// For this test we'll try to keep about 200ms worth of data queued, so the pipe stays full
		const int k_nBufferQueuedTarget = nSendRate / 5;

		// Set the send buffer
		SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendBufferSize, k_nBufferQueuedTarget*5/4 + 1024 );

		// How long do we want to send?
		constexpr SteamNetworkingMicroseconds k_usecSendTime = SteamNetworkingMicroseconds( 10 * 1e6 );

		// Run at this send rate for several seconds
		SteamNetworkingMicroseconds usecStartTime = SteamNetworkingUtils()->GetLocalTimestamp();
		SteamNetworkingMicroseconds usecLastPrint = usecStartTime;
		int64 cbBytesSent = 0;
		int64 cbBytesRecv = 0;
		bool bDrain = false;
		for (;;)
		{
			TEST_PumpCallbacks();

			// Query status
			SteamNetConnectionRealTimeStatus_t serverStatus;
			assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hServer, &serverStatus, 0, nullptr ) );

			SteamNetConnectionRealTimeStatus_t clientStatus;
			assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hClient, &clientStatus, 0, nullptr ) );

			// Time to enter drain mode?
			SteamNetworkingMicroseconds usecNow = SteamNetworkingUtils()->GetLocalTimestamp();
			if ( !bDrain && usecNow > usecStartTime + k_usecSendTime )
			{
				TEST_Printf( "Entering drain mode\n" );
				bDrain = true;
				usecLastPrint = 0;
			}

			// Time to print status?
			if ( usecLastPrint + 500*1000 < usecNow )
			{
				SteamNetworkingMicroseconds usecElapsed = usecNow - usecStartTime;
				assert( usecElapsed < k_usecSendTime * 2 );
				double flElapsedSeconds = usecElapsed * 1e-6;
				TEST_Printf( "Elapsed:%6.0fms   Sent:%7.0fK   Recv:%7.0fK = %5.0fK/sec  (Wire%6.3f kpkts/sec Qual %5.1f%%)\n",
					flElapsedSeconds * 1e3,
					cbBytesSent * 1e-3,
					cbBytesRecv * 1e-3,
					cbBytesRecv * 1e-3 / flElapsedSeconds,
					clientStatus.m_flInPacketsPerSec * 1e-3,
					clientStatus.m_flConnectionQualityLocal * 100.0f
				);
				usecLastPrint = usecNow;
			}

			// On the server, try to keep the buffer full at a certain amount
			if ( !bDrain )
			{
				while ( serverStatus.m_cbPendingReliable + 1024 < k_nBufferQueuedTarget )
				{
					// How much is missing?
					int cbSendMsg = std::min( k_nBufferQueuedTarget - serverStatus.m_cbPendingReliable, k_cbMaxSteamNetworkingSocketsMessageSizeSend );

					// Don't send tiny messages, just wait until we can queue up some more
					if ( cbSendMsg < 1024 )
						break;

					// Allocate a message.
					SteamNetworkingMessage_t *pSendMsg = SteamNetworkingUtils()->AllocateMessage( cbSendMsg );
					pSendMsg->m_conn = hServer;
					pSendMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;
					// Don't bother initializing the body

					int64 nMsgNumberOrResult = 0;
					SteamNetworkingSockets()->SendMessages( 1, &pSendMsg, &nMsgNumberOrResult, true );
					if ( nMsgNumberOrResult == -k_EResultLimitExceeded )
					{
						TEST_Printf( "SendMessage returned limit exceeded trying to queue %d + %d = %d\n", serverStatus.m_cbPendingReliable, cbSendMsg, serverStatus.m_cbPendingReliable + cbSendMsg );
						break;
					}
					assert( nMsgNumberOrResult > 0 );

					serverStatus.m_cbPendingReliable += cbSendMsg + 64;
					cbBytesSent += cbSendMsg;
				}
			}

			// On the client, we'll just periodically send small messages,
			// just to keep some traffic going in the other direction.  This
			// isn't necessary, but it's a slightly more realistic test
			if ( clientStatus.m_cbPendingReliable+clientStatus.m_cbSentUnackedReliable == 0 )
			{
				char dummyMsg[ 1024 ];
				EResult r = SteamNetworkingSockets()->SendMessageToConnection( hClient, dummyMsg, sizeof(dummyMsg), k_nSteamNetworkingSend_Reliable, nullptr );
				assert( k_EResultOK == r );
			}

			// Receive server->client messages
			SteamNetworkingMessage_t *pMsg[ 16 ];
			for (;;)
			{
				int nMsg = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hClient, pMsg, 16 );
				if ( nMsg <= 0 )
				{
					assert( nMsg == 0 );
					break;
				}

				for ( int i = 0 ; i < nMsg ; ++i )
				{
					cbBytesRecv += pMsg[i]->m_cbSize;
					assert( cbBytesRecv <= cbBytesSent );
					pMsg[i]->Release();
				}

				if ( nMsg < 16 )
					break;
			}

			// Receive client->server messages
			for (;;)
			{
				int nMsg = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hServer, pMsg, 16 );
				if ( nMsg <= 0 )
				{
					assert( nMsg == 0 );
					break;
				}

				for ( int i = 0 ; i < nMsg ; ++i )
					pMsg[i]->Release();

				if ( nMsg < 16 )
					break;
			}

			// Done?
			if ( bDrain && cbBytesRecv == cbBytesSent )
				break;
		}

		{
			SteamNetworkingMicroseconds usecNow = SteamNetworkingUtils()->GetLocalTimestamp();
			double flElapsedSeconds = ( usecNow - usecStartTime ) * 1e-6;
			TEST_Printf( "TOTAL:  %6.0fms   Sent:%7.0fK   Recv:%7.0fK = %5.0fK/sec\n\n",
				flElapsedSeconds * 1e3,
				cbBytesSent * 1e-3,
				cbBytesRecv * 1e-3,
				cbBytesRecv * 1e-3 / flElapsedSeconds
			);
		}

	}

	// Cleanup
	SteamNetworkingSockets()->CloseConnection( hServer, 0, nullptr, false );
	SteamNetworkingSockets()->CloseConnection( hClient, 0, nullptr, false );
}

void Test_send_buffer_full()
{
	TEST_Printf( "***************************************************\n" );
	TEST_Printf( "Test: SendMessages batch stop and retry on buffer full\n" );
	TEST_Printf( "***************************************************\n" );

	// Network loopback so we get real rate limiting
	HSteamNetConnection hSender, hRecver;
	assert( SteamNetworkingSockets()->CreateSocketPair( &hSender, &hRecver, true, nullptr, nullptr ) );
	SteamNetworkingSockets()->SetConnectionName( hSender, "sender" );
	SteamNetworkingSockets()->SetConnectionName( hRecver, "recver" );

	// Low send rate so the buffer stays full long enough to observe
	const int k_nSendRate = 64 * 1024;
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendRateMin, k_nSendRate );
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendRateMax, k_nSendRate );

	// Buffer fits one 100K message but not two, so msg[1] overflows and msg[2] is never attempted
	const int k_cbMsg = 100 * 1024;
	const int k_nSendBuffer = 160 * 1024;
	SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendBufferSize, k_nSendBuffer );

	// Prepare three reliable messages
	SteamNetworkingMessage_t *pMessages[3];
	for ( int i = 0; i < 3; ++i )
	{
		pMessages[i] = SteamNetworkingUtils()->AllocateMessage( k_cbMsg );
		pMessages[i]->m_conn = hSender;
		pMessages[i]->m_nFlags = k_nSteamNetworkingSend_Reliable;
	}

	// Send all three at once. Pass bDeleteFailedMessages=false so the library
	// leaves failed/not-attempted message pointers in place for retry.
	int64 results[3] = {};
	SteamNetworkingSockets()->SendMessages( 3, pMessages, results, /*bDeleteFailedMessages=*/false );

	// First message fits in the 160K buffer: should succeed
	assert( results[0] > 0 );
	assert( pMessages[0] == nullptr ); // library took ownership

	// Second message overflows the buffer: should fail
	assert( results[1] == -k_EResultLimitExceeded );
	assert( pMessages[1] != nullptr ); // we still own it; can retry

	// Third message was never attempted because the second failed
	assert( results[2] == 0 );
	assert( pMessages[2] != nullptr ); // we still own it; can retry

	TEST_Printf( "Initial send: msg[0]=#%lld succeeded, msg[1]=LimitExceeded, msg[2]=not attempted.\n", results[0] );

	// Retry the two remaining messages one at a time.  The buffer only holds one
	// 100K message, so we must wait for space before each retry.
	SteamNetworkingMessage_t *pRetry[2] = { pMessages[1], pMessages[2] };
	for ( int i = 0; i < 2; ++i )
	{
		TEST_Printf( "Waiting for buffer space (retry %d)...\n", i );
		SteamNetworkingMicroseconds usecDeadline = SteamNetworkingUtils()->GetLocalTimestamp() + 10 * 1000000LL;
		for (;;)
		{
			TEST_PumpCallbacks();

			// Keep the receiver drained so the connection stays healthy
			SteamNetworkingMessage_t *pRecvMsgs[16];
			for (;;)
			{
				int nRecv = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hRecver, pRecvMsgs, 16 );
				if ( nRecv <= 0 ) break;
				for ( int j = 0; j < nRecv; ++j )
					pRecvMsgs[j]->Release();
				if ( nRecv < 16 ) break;
			}

			SteamNetConnectionRealTimeStatus_t status;
			assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hSender, &status, 0, nullptr ) );
			if ( status.m_cbPendingReliable + k_cbMsg < k_nSendBuffer )
				break;

			assert( SteamNetworkingUtils()->GetLocalTimestamp() < usecDeadline );
		}

		int64 retryResult = 0;
		SteamNetworkingSockets()->SendMessages( 1, &pRetry[i], &retryResult, /*bDeleteFailedMessages=*/false );
		assert( retryResult > 0 );
		assert( pRetry[i] == nullptr );
		TEST_Printf( "Retry %d succeeded: queued as msg #%lld.\n", i, retryResult );
	}

	SteamNetworkingSockets()->CloseConnection( hSender, 0, nullptr, false );
	SteamNetworkingSockets()->CloseConnection( hRecver, 0, nullptr, false );
}

int main( int argc, const char **argv  )
{
	typedef void (*FnTest)(void);
	struct Test_t {
		const char *m_pszName;
		FnTest m_func;
	};

	#define TEST(x) { #x, Test_ ## x }

	static const Test_t tests[] = {
		TEST(identity),
		TEST(quick),
		TEST(soak),
		TEST(netloopback_throughput),
		TEST(lane_quick_queueanddrain),
		TEST(lane_quick_priority_and_background),
		TEST(pipe),
		TEST(send_buffer_full)
	};

	struct Suite_t {
		const char *m_pszName;
		std::vector< Test_t > m_vecTests;
	};
	static const Suite_t test_suites[] = {
		{ "suite-quick", { TEST(identity), TEST(quick), TEST(lane_quick_queueanddrain), TEST(lane_quick_priority_and_background), TEST(pipe), TEST(send_buffer_full) } }
	};

	if ( argc < 2 )
	{
print_usage:
		{
			const char *prog = argv[0];
			while ( strchr( prog, '/' ) )
				prog = strchr( prog, '/' ) + 1;
			while ( strchr( prog, '\\' ) )
				prog = strchr( prog, '\\' ) + 1;
			printf( "Usage: %s test-or-suite-name ...\n", prog );
		}
print_available_tests_and_exit:
		printf( "\n" );
		printf( "Available tests:\n" );
		for ( const Test_t &t: tests )
			printf( "    %s\n", t.m_pszName );
		printf( "Available test suites:\n" );
		for ( const Suite_t &s: test_suites )
			printf( "    %s\n", s.m_pszName );
		return 1;
	}

	std::vector<Test_t> vecTestsToRun;
	for ( int i = 1 ; i < argc ; ++i )
	{
		if ( !strcasecmp( argv[i], "/?" ) || !strcasecmp( argv[i], "-?" ) || !strcasecmp( argv[i], "/?" ) || !strcasecmp( argv[i], "-h" ) || !strcasecmp( argv[i], "--help" ) )
			goto print_usage;

		bool bFound = false;
		for ( const Test_t &t: tests )
		{
			if ( !strcasecmp( argv[i], t.m_pszName ) )
			{
				vecTestsToRun.push_back( t );
				bFound = true;
				break;
			}
		}
		for ( const Suite_t &s: test_suites )
		{
			if ( !strcasecmp( argv[i], s.m_pszName ) )
			{
				for ( const Test_t &t: s.m_vecTests )
					vecTestsToRun.push_back( t );
				bFound = true;
				break;
			}
		}
		if ( !bFound )
		{
			printf( "No such test or suite named '%s' not known\n", argv[i] );
			goto print_available_tests_and_exit;
		}
	}

	// Initialize library
	TEST_Init( nullptr );

	for ( const Test_t &t: vecTestsToRun )
	{
		TEST_Printf( "--------------------------------------\n");
		TEST_Printf( "Running test '%s'\n", t.m_pszName );
		TEST_Printf( "--------------------------------------\n");
		TEST_Printf( "\n");

		// Make sure each test starts with the default config
		ClearConfig();

		// Run the test
		(*t.m_func)();

		TEST_Printf( "\n" );
		TEST_Printf( "Test '%s' completed OK\n\n", t.m_pszName );
	}

	// Shutdown library
	TEST_Kill();
	return 0;
}

#ifdef NN_NINTENDO_SDK
extern "C" void nnMain() { main( 0, nullptr ); }
#endif
