//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//

#include "sv_publishtest.h"
#include "spew.h"
#include "replay/replayutils.h"
#include "replaysystem.h"
#include "sv_basejob.h"
#include "sv_fileservercleanup.h"
#include "tier1/convar.h"
#include "sv_filepublish.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

//----------------------------------------------------------------------------------------

const char *g_pAcceptableFileserverProtocols[] = { "http", "https", NULL };
const char *g_pAcceptableOffloadProtocols[] = { "ftp", NULL };

//----------------------------------------------------------------------------------------

class CPublishTester
{
public:
	CPublishTester();
	~CPublishTester();

	bool Go();

private:
	bool Test_Emptyness( const char *pDescription, const char *pStr, bool bPrintResult );
	bool Test_Hostname( const char *pHostname, const char *pProtocolExample );
	bool Test_Protocol( const char *pDescription, const char *pProtocol, const char **pAcceptableProtocols );
	bool Test_Port( int nPort );
	bool Test_Path( const char *pDescription, const char *pPath, bool bForwardSlashesAllowed, bool bBackslashesAllowed );
	bool Test_LocalWebServerCVars();
	bool Test_IO( const char *pFilename );
	bool Test_FilePublish( const char *pFilename, bool bOffload );
	bool Test_PublishedFileDelete( const char *pFullFilename, const char *pFilename, bool bOffload );
	bool Test_WaitingForPlayersCVar();
	void PrintBaseUrlWarning();

	char		*m_pGarbageBuffer;
	CBaseJob	*m_pJob;
	CBaseJob	*m_pCleanerJob;
};

//----------------------------------------------------------------------------------------

#define GARBAGE_BUFFER_SIZE			( 1024 * 1000 )

CPublishTester::CPublishTester()
:	m_pGarbageBuffer( NULL ),
	m_pJob( NULL ),
	m_pCleanerJob( NULL )
{
	m_pGarbageBuffer = new char[ GARBAGE_BUFFER_SIZE ];
}

CPublishTester::~CPublishTester()
{
	delete [] m_pGarbageBuffer;
	
	if ( m_pJob )
	{
		m_pJob->Release();
	}

	if ( m_pCleanerJob )
	{
		m_pCleanerJob->Release();
	}
}

bool CPublishTester::Test_Hostname( const char *pHostname, const char *pProtocolExample )
{
	if ( !Test_Emptyness( "Hostname", pHostname, false ) )
		return false;

	if ( V_strstr( pHostname, "://" ) )
	{
		g_pBlockSpewer->PrintEventResult( false );
		CFmtStr fmtError( "Should not contain a protocol (e.g: %s)!", pProtocolExample );
		g_pBlockSpewer->PrintEventError( fmtError.Access() );
		return false;
	}

	// Test IP lookup
	char szIP[16];
	if ( !g_pEngine->NET_GetHostnameAsIP( pHostname, szIP, sizeof( szIP ) ) )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "DNS lookup failed!" );
		return false;
	}

	g_pBlockSpewer->PrintEventResult( true );
	g_pBlockSpewer->PrintEmptyLine();

	return true;
}

bool CPublishTester::Test_Emptyness( const char *pDescription, const char *pStr, bool bPrintResult )
{
	g_pBlockSpewer->PrintValue( pDescription, pStr );
	g_pBlockSpewer->PrintEventStartMsg( "Validating" );

	if ( V_strlen( pStr ) == 0 )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "Empty!" );
		return false;
	}

	if ( bPrintResult )
	{
		g_pBlockSpewer->PrintEventResult( true );
		g_pBlockSpewer->PrintEmptyLine();
	}

	return true;
}

bool CPublishTester::Test_Port( int nPort )
{
	g_pBlockSpewer->PrintValue( "Port", Replay_va( "%i", nPort ) );
	g_pBlockSpewer->PrintEventStartMsg( "Validating" );

	if ( nPort < 0 || nPort > 65535 )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "Port must be between 0 and 65535." );
		return false;
	}

	g_pBlockSpewer->PrintEventResult( true );
	g_pBlockSpewer->PrintEmptyLine();

	return true;
}

bool CPublishTester::Test_Path( const char *pDescription, const char *pPath, bool bForwardSlashesAllowed, bool bBackslashesAllowed )
{
	g_pBlockSpewer->PrintValue( pDescription, pPath );
	g_pBlockSpewer->PrintEventStartMsg( "Validating" );
	if ( V_strlen( pPath ) == 0 )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "Empty path not allowed." );
		return false;
	}

	if ( !bBackslashesAllowed && V_strstr( pPath, "\\" ) )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "Backslashes not allowed!" );
		return false;
	}

	if ( !bForwardSlashesAllowed && V_strstr( pPath, "/" ) )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "Forward slashes not allowed!" );
		return false;
	}

	if ( V_strstr( pPath, "//" ) || V_strstr( pPath, "\\\\" ) )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "Double slash detected!" );
		return false;
	}

	if ( V_strstr( pPath, ".." ) )
	{
		g_pBlockSpewer->PrintEventResult( false );
		g_pBlockSpewer->PrintEventError( "\"..\" not allowed!" );
		return false;
	}

	g_pBlockSpewer->PrintEventResult( true );
	g_pBlockSpewer->PrintEmptyLine();

	return true;
}

bool CPublishTester::Test_Protocol( const char *pDescription, const char *pProtocol, const char **pAcceptableProtocols )
{
	g_pBlockSpewer->PrintValue( pDescription, pProtocol );
	g_pBlockSpewer->PrintEventStartMsg( "Validating" );

	// Test to see if the input protocol is acceptable
	bool bProtocolOK = false;
	int i = 0;
	while ( pAcceptableProtocols[ i ] )
	{
		if ( V_strcmp( pAcceptableProtocols[ i++ ], pProtocol ) == 0 )
		{
			bProtocolOK = true;
			break;
		}
	}

	// Protocol allowed?
	if ( !bProtocolOK )
	{
		g_pBlockSpewer->PrintEventResult( false );
		CFmtStr fmtError( "Must be one of the following (case-sensitive): " );
		i = 0;
		while ( pAcceptableProtocols[ i ] )
			fmtError.AppendFormat( "\"%s\" ", pAcceptableProtocols[ i++ ] );
		g_pBlockSpewer->PrintEventError( fmtError.Access() );
		return false;
	}

	g_pBlockSpewer->PrintEventResult( true );
	g_pBlockSpewer->PrintEmptyLine();

	return true;
}


bool CPublishTester::Test_LocalWebServerCVars()
{
	// NOTE: We use the raw cvar here as opposed to CServerReplayContext::GetLocalFileServerPath(),
	// which actually fixes slashes.  If the cvar is using incorrect slashes here for the given OS,
	// this test will fail with a specific error message.
	extern ConVar replay_local_fileserver_path;
	if ( !Test_Path( "Path", replay_local_fileserver_path.GetString(), IsPosix(), IsWindows() ) )
		return false;

	return true;
}

bool CPublishTester::Test_IO( const char *pFilename )
{
	g_pBlockSpewer->PrintTestHeader( "Testing File I/O" );

	// Print out temp directory so the context for this section is clear
	g_pBlockSpewer->PrintValue( "Temp path", SV_GetTmpDir() );
	g_pBlockSpewer->PrintEmptyLine();

	// Open the file
	FileHandle_t hTmpFile = g_pFullFileSystem->Open( pFilename, "wb+" );
	g_pBlockSpewer->PrintEventStartMsg( "Opening temp file" );
	if ( !hTmpFile )
	{
		g_pBlockSpewer->PrintEventResult( false );
		return false;
	}
	g_pBlockSpewer->PrintEventResult( true );

	// Write the file
	g_pBlockSpewer->PrintEventStartMsg( "Allocating test buffer" );	// Lie.
	if ( !m_pGarbageBuffer )
	{
		g_pBlockSpewer->PrintEventResult( false );
		return false;
	}
	g_pBlockSpewer->PrintEventResult( true );

	g_pBlockSpewer->PrintEventStartMsg( "Writing temp file" );
	if ( g_pFullFileSystem->Write( m_pGarbageBuffer, GARBAGE_BUFFER_SIZE, hTmpFile ) != GARBAGE_BUFFER_SIZE )
	{
		g_pBlockSpewer->PrintEventResult( false );
		return false;
	}
	g_pBlockSpewer->PrintEventResult( true );

	// Close the file
	g_pFullFileSystem->Close( hTmpFile );
	
	return true;
}

bool CPublishTester::Test_FilePublish( const char *pFilename, bool bOffload )
{
	g_pBlockSpewer->PrintTestHeader( "Testing file publisher" );
	g_pBlockSpewer->PrintValue( "Fileserver type", "Local Web server" );

	if ( !Test_LocalWebServerCVars() )
		return false;

	m_pJob = SV_CreateLocalPublishJob( pFilename );

	g_pBlockSpewer->PrintEmptyLine();

	// Run publish test
	if ( !m_pJob || !SV_RunJobToCompletion( m_pJob ) )
	{
		g_pBlockSpewer->PrintEventError( m_pJob->GetErrorStr() );
		return false;
	}

	return true;
}

bool CPublishTester::Test_PublishedFileDelete( const char *pFullFilename, const char *pFilename, bool bOffload )
{
	g_pBlockSpewer->PrintTestHeader( "Testing fileserver delete" );

	if ( bOffload )
	{
		// Delete the file from the tmp dir
		g_pFullFileSystem->RemoveFile( pFullFilename );
	}

	m_pCleanerJob = SV_CreateDeleteFileJob();
	IFileserverCleanerJob *pCleanerJobImp = SV_CastJobToIFileserverCleanerJob( m_pCleanerJob );
	pCleanerJobImp->AddFileForDelete( pFilename );
	if ( !m_pCleanerJob || !SV_RunJobToCompletion( m_pCleanerJob ) )
	{
		g_pBlockSpewer->PrintEventError( m_pCleanerJob->GetErrorStr() );
		return false;
	}

	return true;
}

bool CPublishTester::Test_WaitingForPlayersCVar()
{
	ConVarRef mp_waitingforplayers_cancel( "mp_waitingforplayers_cancel" );
	if ( mp_waitingforplayers_cancel.IsValid() && mp_waitingforplayers_cancel.GetBool() )
	{
		g_pBlockSpewer->PrintEventError( "mp_waitingforplayers_cancel must be 0 in order for replay to work!" );
		return false;
	}

	return true;
}

void CPublishTester::PrintBaseUrlWarning()
{
	g_pBlockSpewer->PrintEmptyLine();
	g_pBlockSpewer->PrintMsg( "If clients can't access the following URL via a Web" );
	g_pBlockSpewer->PrintMsg( "browser, they will not be able to download Replays." );
	g_pBlockSpewer->PrintEmptyLine();
	g_pBlockSpewer->PrintValue( "URL", Replay_GetDownloadURL() );
}

bool CPublishTester::Go()
{
	const bool bOffload = false;

	// Force anyone outside of Go() to use the block spewer until we're done.
	CSpewScope SpewScope( g_pBlockSpewer );

	g_pBlockSpewer->PrintMsg( "TESTING REPLAY SYSTEM CONFIGURATION..." );

	// Fileserver convars
	g_pBlockSpewer->PrintTestHeader( "Testing Fileserver ConVars (replay_fileserver_*)" );

	// Test replay_fileserver_protocol
	extern ConVar replay_fileserver_protocol;
	if ( !Test_Protocol( "Protocol", replay_fileserver_protocol.GetString(), g_pAcceptableFileserverProtocols ) )
		return false;

	extern ConVar replay_fileserver_host;
	if ( !Test_Hostname( replay_fileserver_host.GetString(), "\"http\" or \"https\"" ) )
		return false;

	extern ConVar replay_fileserver_port;
	if ( !Test_Port( replay_fileserver_port.GetInt() ) )
		return false;

	extern ConVar replay_fileserver_path;
	if ( !Test_Path( "Path", replay_fileserver_path.GetString(), true, false ) )
		return false;

	// Print out the base URL / warning
	PrintBaseUrlWarning();

	CFmtStr fmtTmpFilename( "testpublish_%i.tmp", (int)abs( rand() % 10000 ) );
	CFmtStr fmtTmpFilenameFullPath( "%s%s", SV_GetTmpDir(), fmtTmpFilename.Access() );
	const char *pFilename = fmtTmpFilenameFullPath.Access();

	// Test file I/O
	if ( !Test_IO( pFilename ) )
		return false;

	// Get out if necessary
	if ( !Test_FilePublish( pFilename, bOffload ) )
		return false;

	// Test delete from fileserver
	if ( !Test_PublishedFileDelete( pFilename, fmtTmpFilename.Access(), bOffload ) )
		return false;

	// Make sure mp_waitingforplayers_cancel isn't on or replay will be fucked.
	if ( !Test_WaitingForPlayersCVar() )
		return false;

	return true;
}

//----------------------------------------------------------------------------------------

bool SV_DoTestPublish()
{
	CPublishTester tester;
	return tester.Go();
}

//----------------------------------------------------------------------------------------