#include "TimeoutAqlTest.hpp"
#include <chrono>
#include <cstring>
#include <exception>
#include <fstream>
#include <signal.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include <ext/exception>
#include <ext/random>

#include "AqlTest.hpp"
#include "common/ResultInterpret.h"
#include "global/GlobalData.h"
#include "parser/Parser.h"
#include "readline/IstreamLineInterface.h"
#include "readline/StringLineInterface.h"

#define PIPE_RD 0
#define PIPE_WR 1

#define FD_STDOUT 1
#define FD_STDERR 2

/* Communication between signal handler and the rest of the program */
std::array < int, 2 > g_Wakeup;  // pipe for wakeup
bool g_RecvSignal; // signalled flag

bool waitSignalTimeout ( const std::chrono::microseconds& duration ) {
	struct timeval tv;
	fd_set rd;

	auto duration_secs = std::chrono::duration_cast < std::chrono::seconds > ( duration );
	auto duration_usecs = std::chrono::duration_cast < std::chrono::microseconds > ( duration - duration_secs );
	tv.tv_sec = duration_secs.count ( );
	tv.tv_usec = duration_usecs.count ( );
	FD_ZERO ( &rd );
	FD_SET ( g_Wakeup [ PIPE_RD ], &rd );

	select ( g_Wakeup [ PIPE_RD ] + 1, &rd, nullptr, nullptr, &tv );
	return g_RecvSignal;
}

std::string readFromFD ( int fd ) {
	static const size_t BUFSIZE = 64;

	std::string res;
	int rd;
	std::array < char, BUFSIZE > buf;

	while ( ( rd = read ( fd, buf.data ( ), buf.size ( ) - 1 ) ) > 0 ) {
		res.append ( buf.data ( ), rd );
	}

	return res;
}

void writeToFD ( int fd, const std::string & message, const std::string & errorDesc ) {
	if ( write ( fd, message.c_str ( ), message.length ( ) ) != static_cast < ssize_t > ( message.length ( ) ) )
		throw std::runtime_error ( "TimeoutAqlTest: write() to pipe failed (" + errorDesc + ")" );
}

void newSigChild ( int ) {
	g_RecvSignal = true;

	// write into the pipe so select can read something, this effectively means that SIGCHILD was raised
	writeToFD ( g_Wakeup [ PIPE_WR ], " ", "wakeup signalling" );
}

struct ChildStatus {
	int status;
	unsigned seed;
	std::string outAql, outStdout, outStderr;
};

ChildStatus TimeoutAqlTestImpl ( const std::chrono::microseconds & timeout, std::istream& is ) {
	/* Register SIGCHLD handler */
	struct sigaction act;
	memset ( &act, 0, sizeof ( act ) );
	act . sa_handler = newSigChild;
	sigaction ( SIGCHLD, &act, nullptr );

	std::array < int, 2 > pipeAqlOutput; /* parent-child communication ( aql output ) */
	std::array < int, 2 > pipeStdout;    /* parent-child communication ( child stdout ) */
	std::array < int, 2 > pipeStderr;    /* parent-child communication ( child stderr ) */

	if ( pipe ( pipeAqlOutput.data ( ) ) != 0 )
		throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (aql output)" );

	if ( pipe ( pipeStdout. data ( ) ) != 0 )
		throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (child stdout)" );

	if ( pipe ( pipeStderr. data ( ) ) != 0 )
		throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (child stderr)" );

	/* SIGCHLD was not yet raised, initialize communication pipe */
	g_RecvSignal = false;
	if ( pipe ( g_Wakeup.data ( ) ) )
		throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (wakeup signalling)" );

	/* random seed for aql */
	unsigned seed = ext::random_devices::random ( );

	/* do the forking */
	pid_t pid = fork ();
	REQUIRE ( pid >= 0 );

	if ( pid == 0 ) { /* child, run the test here */
		act . sa_handler = SIG_DFL;
		sigaction ( SIGCHLD, &act, nullptr );

		/* close unused ends of pipes in child */
		close ( g_Wakeup [ PIPE_RD ] );
		close ( g_Wakeup [ PIPE_WR ] );
		close ( pipeAqlOutput [ PIPE_RD ] );

		close ( pipeStdout [ PIPE_RD ] );
		close ( pipeStderr [ PIPE_RD ] );

		/* redirect stderr and stdout to pipe */
		dup2 ( pipeStdout [ PIPE_WR ], FD_STDOUT );
		dup2 ( pipeStderr [ PIPE_WR ], FD_STDERR );

		/* run test */
		AqlTestResult res = AqlTest ( is, seed );
		writeToFD ( pipeAqlOutput [ PIPE_WR ], res.output, "writing cli output" );
		exit ( res.retcode );
	}

	/* close unused ends of pipes in parent */
	close ( pipeAqlOutput [ PIPE_WR ] );
	close ( pipeStdout [ PIPE_WR ] );
	close ( pipeStderr [ PIPE_WR ] );

	/* lets wait the specified time of microseconds, maybe the child will terminate on its own */
	if ( ! waitSignalTimeout ( timeout ) ) {
		/* ... and in case it did not ... */
		kill ( pid, SIGTERM );
		while ( ! waitSignalTimeout ( 250ms ) ) /* 1/4 second */
			kill ( pid, SIGKILL );
	}

	/* child termination confirmed */
	ChildStatus status;

	status.seed = seed;

	waitpid ( pid, &status.status, 0 );
	close ( g_Wakeup [ PIPE_RD ] );
	close ( g_Wakeup [ PIPE_WR ] );

	/* read child outputs */
	status.outAql = readFromFD ( pipeAqlOutput [ PIPE_RD ] );
	status.outStdout = readFromFD ( pipeStdout [ PIPE_RD ] );
	status.outStderr = readFromFD ( pipeStderr [ PIPE_RD ] );

	/* communication is done */
	close ( pipeAqlOutput [ PIPE_RD ] );
	close ( pipeStdout [ PIPE_RD ] );
	close ( pipeStderr [ PIPE_RD ] );

	return status;
}

void processTestChildStatus ( const ChildStatus & status, const std::chrono::microseconds & timeout, bool timeoutError, const std::string & test ) {
	enum class ChildTerminationStatus {
		Signal,
		Timeout,
		OK,
		Fail,
	} terminationState = ChildTerminationStatus::OK; /* make compiler happy */

	/* determine test result */
	if ( WIFSIGNALED ( status.status ) && ( WTERMSIG ( status.status ) == SIGTERM || WTERMSIG ( status.status ) == SIGKILL ) )
		terminationState = ChildTerminationStatus::Timeout;
	else if ( WIFSIGNALED ( status.status ) && WTERMSIG ( status.status ) != SIGTERM && WTERMSIG ( status.status ) != SIGKILL )
		terminationState = ChildTerminationStatus::Signal;
	else if ( WIFEXITED ( status.status ) && WEXITSTATUS ( status.status ) == 0 )
		terminationState = ChildTerminationStatus::OK;
	else if ( WIFEXITED ( status.status ) && WEXITSTATUS ( status.status ) != 0 )
		terminationState = ChildTerminationStatus::Fail;
	else
		FAIL ( );

	/* format message */
	std::ostringstream oss;
	oss << "AqlTest: ";
	if ( terminationState == ChildTerminationStatus::Timeout ) {
		oss << "timeout (" << timeout.count ( ) << " us) reached";
	} else if ( terminationState == ChildTerminationStatus::Signal ) {
		oss << "killed by signal " << WTERMSIG ( status.status ) << " (" << strsignal ( WTERMSIG ( status.status ) ) << ")";
	} else if ( terminationState == ChildTerminationStatus::OK) {
		oss << "completed succesfully";
	} else if ( terminationState == ChildTerminationStatus::Fail ) {
		oss << "completed unsuccessfully";
	} else {
		FAIL ( );
	}
	oss << "\n";
	oss << test << "\n";
	oss << "Seed was: " << status.seed << "\n";
	oss << "Child aqlout was: >" << status.outAql << "<\n";
	oss << "Child stdout was: >" << status.outStdout << "<\n";
	oss << "Child stderr was: >" << status.outStderr << "<\n";

	/* process result */
	if ( terminationState == ChildTerminationStatus::Timeout ) {
		WARN ( oss.str ( ) );
		if ( timeoutError ) {
			FAIL ( );
		} else {
			CHECK_NOFAIL ( "Timeout reached" );
		}
	} else if ( terminationState == ChildTerminationStatus::Signal ) {
		WARN ( oss.str ( ) );
		FAIL ( );
	} else if ( terminationState == ChildTerminationStatus::OK ) {
		// pass
	} else if ( terminationState == ChildTerminationStatus::Fail ) {
		WARN ( oss.str ( ) );
		REQUIRE ( WEXITSTATUS ( status.status ) == 0 );
	} else {
		FAIL ( );
	}
}

std::string printTest ( const std::filesystem::path & file ) {
	return "Trying to execute testfile " + std::string ( file ) + "\n";
}

std::string printTest ( const std::vector < std::string > & queries ) {
	std::ostringstream oss;
	oss << "Trying to execute queries:\n";
	for ( const std::string & query : queries )
		oss << "  " << query << "\n";
	return oss.str ( );
}

void TimeoutAqlTestInt ( const std::chrono::microseconds & timeout, const std::filesystem::path & file, bool timeoutError ) {
	std::ifstream ifs ( file );
	REQUIRE ( ifs.is_open ( ) );
	auto testChildStatus = TimeoutAqlTestImpl ( timeout, ifs );
	processTestChildStatus ( testChildStatus, timeout, timeoutError, printTest ( file ) );
}

void TimeoutAqlTestInt ( const std::chrono::microseconds & timeout, const std::vector < std::string > & queries, bool timeoutError ) {
	std::stringstream ifs;
	for ( const auto & q : queries )
		ifs << q << "\n";

	auto testChildStatus = TimeoutAqlTestImpl ( timeout, ifs );
	processTestChildStatus ( testChildStatus, timeout, timeoutError, printTest ( queries ) );
}