/* Web Polygraph       http://www.web-polygraph.org/
 * (C) 2003-2006 The Measurement Factory
 * Licensed under the Apache License, Version 2.0 */

#include "base/polygraph.h"

#include "xstd/h/iomanip.h"

#include "xstd/gadgets.h"
#include "base/polyLogCats.h"
#include "base/polyVersion.h"
#include "runtime/HttpDate.h"
#include "xml/XmlAttr.h"
#include "xml/XmlNodes.h"
#include "xml/XmlTable.h"
#include "xml/XmlSection.h"
#include "xml/XmlParagraph.h"
#include "xml/XmlText.h"
#include "loganalyzers/ReportBlob.h"
#include "loganalyzers/BlobDb.h"
#include "loganalyzers/InfoScopes.h"
#include "loganalyzers/PhaseInfo.h"
#include "loganalyzers/SideInfo.h"
#include "loganalyzers/TestInfo.h"

static XmlAttr algnLeft("align", "left");
static XmlAttr algnRight("align", "right");


TestInfo::TestInfo(const String &aLabel): theLabel(aLabel), theSides(lgcEnd) {
	theSides.count(lgcEnd);
	theSides[lgcCltSide] = new SideInfo(lgcCltSide);
	theSides[lgcCltSide]->test(this);
	theSides[lgcSrvSide] = new SideInfo(lgcSrvSide);
	theSides[lgcSrvSide]->test(this);
	theExecScope.addSide("client");
	theExecScope.addSide("server");
	theExecScope.name("baseline");
}

TestInfo::~TestInfo() {
	while (theSides.count()) {
		if (theSides.last())
			theSides.last()->test(0);
		delete theSides.pop();
	}

	while (theScopes.count()) delete theScopes.pop();
}

void TestInfo::execScope(const Scope &aScope) {
	theExecScope = aScope;
}

const TestInfo::Scope &TestInfo::guessExecScope() {
	Assert(!theExecScope);
	const SideInfo &side = aSide();

	// find last phase with peak (highest) request rate
	String bestName;
	double peakRate = -1;
	for (int i = 0; i < side.phaseCount(); ++i) {
		const PhaseInfo &phase = side.phase(i);
		const double rate = phase.availStats().reqRate();
		// allow for 1% rate diff among phases with the same configured rate
		if (!bestName || peakRate <= 1.01*rate) {
			peakRate = rate;
			bestName = phase.name();
		}
	}

	if (Should(bestName)) {
		clog << "no executive summary phases specified, using '" 
			<< bestName << "' phase" << endl;
		theExecScope.addPhase(bestName);
	}

	return theExecScope;
}

const String &TestInfo::label() const {
	return theLabel;
}

const String &TestInfo::pglCfg() const {
	return thePglCfg;
}

Time TestInfo::startTime() const {
	return theStartTime;
}

const InfoScope &TestInfo::execScope() const {
	return theExecScope;
}

const SideInfo *TestInfo::cltSideExists()  const {
	return side(lgcCltSide).procCount() ? theSides[lgcCltSide] : 0;
}

const SideInfo *TestInfo::srvSideExists() const {
	return side(lgcSrvSide).procCount() ? theSides[lgcSrvSide] : 0;
}

SideInfo &TestInfo::cltSide() {
	return side(lgcCltSide);
}

SideInfo &TestInfo::srvSide() {
	return side(lgcSrvSide);
}

SideInfo &TestInfo::side(int logCat) {
	Assert(logCat == lgcCltSide || logCat == lgcSrvSide);
	Assert(theSides[logCat]);
	return *theSides[logCat];
}

const SideInfo &TestInfo::aSide() const {
	return cltSideExists() ? cltSide() : srvSide();
}

const SideInfo &TestInfo::cltSide() const {
	return side(lgcCltSide);
}

const SideInfo &TestInfo::srvSide() const {
	return side(lgcSrvSide);
}

const SideInfo &TestInfo::side(int logCat) const {
	Assert(logCat == lgcCltSide || logCat == lgcSrvSide);
	Assert(theSides[logCat]);
	return *theSides[logCat];
}

int TestInfo::scopes(InfoScopes &res) const {
	if (!twoSided())
		return aSide().scopes(res);

	for (int i = 0; i < theScopes.count(); ++i)
		res.add(*theScopes[i]);

	return res.count();
}

void TestInfo::checkCommonPglCfg() {
	if (!cltSideExists() && srvSideExists())
		thePglCfg = srvSide().pglCfg();
	else
	if (cltSideExists() && !srvSideExists())
		thePglCfg = cltSide().pglCfg();
	else
	if (cltSide().pglCfg() == srvSide().pglCfg())
		thePglCfg = cltSide().pglCfg();
	else {
		cerr << label() << ": warning: client- and server-side PGL configurations"
			<< " differ" << endl;
		thePglCfg = String();
	}
}

void TestInfo::checkCommonBenchmarkVersion() {
	if (!cltSideExists() && srvSideExists())
		theBenchmarkVersion = srvSide().benchmarkVersion();
	else
	if (cltSideExists() && !srvSideExists())
		theBenchmarkVersion = cltSide().benchmarkVersion();
	else
	if (cltSide().benchmarkVersion() == srvSide().benchmarkVersion()) {
		theBenchmarkVersion = cltSide().benchmarkVersion();
	}
	else {
		cerr << label() << ": warning: client- and server-side"
			<< " benchmark versions differ" << endl;
		theBenchmarkVersion = String();
	}
}

void TestInfo::checkCommonStartTime() {

	if (!cltSideExists() && srvSideExists())
		theStartTime = srvSide().startTime();
	else
	if (cltSideExists() && !srvSideExists())
		theStartTime = cltSide().startTime();
	else 
	if (cltSide().startTime() >= 0 && srvSide().startTime() >= 0) {
		const Time diff = 
			Max(cltSide().startTime(), srvSide().startTime()) -
			Min(cltSide().startTime(), srvSide().startTime());
		if (diff > Time::Sec(5*60))
			cerr << label() << ": warning: client- and server-side process "
				<< "start times differ by " << diff << endl;
		// should we not set a start time in this case (like with thePglCfg)
		theStartTime = Min(cltSide().startTime(), srvSide().startTime());
	}
}

void TestInfo::checkConsistency() {
	if (!cltSideExists() && !srvSideExists()) {
		cerr << "no client- or server-side information found in the logs, exiting" 
			<< endl << xexit;
	}

	if (!cltSideExists() || !srvSideExists()) {
		const String &sname = cltSideExists() ?
			cltSide().name() : srvSide().name();
		theOneSideWarn = String("Only ") + sname + 
			"-side information found in the logs.";
		cerr << "warning: " << theOneSideWarn
			<< "  The report will be incomplete and less accurate" << endl;
	}

	if (cltSideExists())
		cltSide().checkConsistency();
	if (srvSideExists())
		srvSide().checkConsistency();

	checkCommonBenchmarkVersion();
	checkCommonPglCfg();
	checkCommonStartTime();
	//checkCommonPhases();
}

int TestInfo::repCount(const Scope &scope) const {
	return cltSideExists() ? cltSide().repCount(scope) : -1;
}

int TestInfo::hitCount(const Scope &scope) const {
	return twoSided() ? cltSide().repCount(scope) - srvSide().repCount(scope) : -1;
}

BigSize TestInfo::repVolume(const Scope &scope) const {
	return cltSideExists() ? cltSide().repVolume(scope) : BigSize();
}

BigSize TestInfo::hitVolume(const Scope &scope) const {
	return twoSided() ? 
		cltSide().repVolume(scope) - srvSide().repVolume(scope) :
		BigSize();
}

void TestInfo::cmplExecSumVars(BlobDb &db) {
	static const String tlLabel = "test label";
	addMeasBlob(db, "label", theLabel, "string", tlLabel);

	{
		ostringstream buf;
		HttpDatePrint(buf, startTime());
		buf << ends;
		static const String tlStartTime = "test start time";
		addMeasBlob(db, "start.time", buf.str().c_str(), "string", tlStartTime);
		streamFreeze(buf, false);
	}

	{
		static const String tlTitle = "benchmark software version";
		ReportBlob blob("benchmark.version" + theExecScope, tlTitle);
		if (theBenchmarkVersion) {
			blob << XmlText(theBenchmarkVersion);
		} else {
			XmlParagraph p;
			p << XmlText("cannot show a single benchmark version because ");
			p << db.ptr(BlobDb::Key("benchmark.version", execScope().oneSide("client")), XmlText("client-"));
			p << XmlText(" and ");
			p << db.ptr(BlobDb::Key("benchmark.version", execScope().oneSide("server")), XmlText("server-side"));
			p << XmlText(" versions differ");
			blob << p;
		}
		db << blob;
	}

	{
		static const String tlTitle = "reporter software version";
		ReportBlob blob("reporter.version" + theExecScope, tlTitle);
		blob << XmlText(PolyVersion());
		db << blob;
	}
}

void TestInfo::cmplExecSum(BlobDb &db) {
	const Scope &cltScope = theExecScope.oneSide("client");

	static const String tlTitle = "executive summary";
	ReportBlob blob("summary.exec.table" + theExecScope, tlTitle);
	blob << XmlAttr("vprimitive", "Test summary");

	XmlTable table;
	table << XmlAttr::Int("border", 0);

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("label:");

		XmlTableCell cell;
		cell << db.include("label");
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("throughput:");

		XmlTableCell cell;
		cell << db.quote("rep.rate" + cltScope);
		cell << XmlText(" or ");
		cell << db.quote("rep.bwidth" + cltScope);
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("response time:");

		XmlTableCell cell;
		//cell << db.quote("object.hits.rptm.mean" + cltScope);
		//cell << XmlText(" hit, ");
		cell << db.quote("rep.rptm.mean" + cltScope);
		cell << XmlText(" mean");
		//cell << db.quote("object.misses.rptm.mean" + cltScope);
		//cell << XmlText(" miss");
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("hit ratios:");

		XmlTableCell cell;
		cell << db.quote("hit.ratio.obj" + theExecScope);
		cell << XmlText(" DHR and ");
		cell << db.quote("hit.ratio.byte" + theExecScope);
		cell << XmlText(" BHR");
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("errors:");

		XmlTableCell cell;
		cell << db.quote("xact.error.ratio" + cltScope);
		cell << XmlText(" (");
		cell << db.quote("xact.error.count" + cltScope);
		cell << XmlText(" out of ");
		cell << db.quote("xact.count" + cltScope);
		cell << XmlText(")");
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("duration:");

		XmlTableCell cell;
		cell << db.include("duration" + cltScope);
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("start time:");

		XmlTableCell cell;
		cell << db.include("start.time");
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("workload:");

		XmlTableCell cell;
		cell << db.ptr("workload" + theExecScope, XmlText("available"));
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("Polygraph version:");

		XmlTableCell cell;
		cell << db.include("benchmark.version" + theExecScope);
		tr << cell;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("reporter version:");

		XmlTableCell cell;
		cell << db.include("reporter.version" + theExecScope);
		tr << cell;

		table << tr;
	}

	blob << table;

	{ // XXX: out of place (this is not a "table record")
		XmlParagraph p;
		XmlText text;
		text.buf() << "This executive summary and baseline report statistics"
			<< " are based on the following "
			<< theExecScope.phases().count() << " test phase(s): ";

		{for (int i = 0; i < theExecScope.phases().count(); ++i) {
			if (i)
				text.buf() << ", ";
			text.buf() << *theExecScope.phases().item(i);
		}}

		text.buf() << ". The test has the following " 
			<< aSide().phaseCount() << " phase(s): ";

		{for (int i = 0; i < aSide().phaseCount(); ++i) {
			if (i)
				text.buf() << ", ";
			text.buf() << aSide().phase(i).name();
		}}
		text.buf() << '.';

		p << text;
		blob << p;
	}

	db << blob;
}

void TestInfo::cmplWorkload(BlobDb &db) {
	static const String tlTitle = "test workload";
	ReportBlob blob(BlobDb::Key("workload", theExecScope), tlTitle);

	if (!thePglCfg) {
		XmlParagraph p;
		p << XmlText("cannot show a single test workload because ");
		p << db.ptr(BlobDb::Key("workload.code", execScope().oneSide("client")), XmlText("client-"));
		p << XmlText(" and ");
		p << db.ptr(BlobDb::Key("workload.code", execScope().oneSide("server")), XmlText("server-side"));
		p << XmlText(" PGL configurations differ");
		blob << p;
		return;
	}

	{
		XmlSection sect("English interpretation");
		sect << XmlTextTag<XmlParagraph>("TBD.");
		blob << sect;
	}

	{
		XmlSection sect("PGL code");

		static const String tlPglTitle = "PGL code";
		ReportBlob code(BlobDb::Key("workload.code", theExecScope), tlPglTitle);
		XmlTag codesample("codesample");
		codesample << XmlText(thePglCfg);
		code << codesample;
		db << code;

		sect << code;
		blob << sect;
	}

	db << blob;
}

void TestInfo::cmplHitRatioVars(BlobDb &db, const Scope &scope) {
	if (twoSided()) {
		const String sfx = BlobDb::KeySuffix(scope);
		const double dhr = Percent(hitCount(scope), repCount(scope));
		static const String tlDhr = "document hit ratio";
		addMeasBlob(db, "hit.ratio.obj" + sfx, dhr, "%", tlDhr);
		const double bhr = Percent(hitVolume(scope).byted(), repVolume(scope).byted());
		static const String tlBhr = "byte hit ratio";
		addMeasBlob(db, "hit.ratio.byte" + sfx, bhr, "%", tlBhr);
	} else {
		// XXX: put err pointer to the theOneSideWarn-based description
		Should(false);
	}
}

void TestInfo::cmplHitRatio(BlobDb &db, const Scope &scope) {
	static const String tlTitle = "hit ratios";
	ReportBlob blob(BlobDb::Key("hit.ratio", scope), tlTitle);
	blob << XmlAttr("vprimitive", "Hit Ratios");

	if (twoSided()) {

		cmplHitRatioTable(db, blob, scope);

		{
			XmlTag descr("description");

			XmlTextTag<XmlParagraph> p1;
			p1.buf() << "The hit ratios table shows measured hit "
				<< "ratios. Hits are calculated based on client- and "
				<< "server-side traffic comparison. Offered hits are "
				<< "counted for 'basic' transactions only (simple HTTP GET "
				<< "requests with '200 OK' responses). Measured hit stats "
				<< "are based on all transactions. Thus, 'offered' hit ratio "
				<< "are not the same as 'ideal' hit ratio in this context. ";
			descr << p1;

			XmlTextTag<XmlParagraph> p2;
			p2.buf() << "Measured hit count or volume is the difference "
				<< "between client- and server-side traffic counts or "
				<< "volumes. "
				<< "DHR, Document Hit Ratio, is the ratio of the total "
				<< "number of hits to the number of all transactions. "
				<< "BHR, Byte Hit Ratio, is the ratio of "
				<< "the total volume (a sum of response sizes) of hits to the "
				<< "total volume of all transactions. "
				<< "Negative measured hit ratios are possible if server-side "
				<< "traffic of a cache exceeds client-side traffic (e.g., "
				<< "due to optimistic prefetching or extra freshness checks) "
				<< "and if side measurements are out-of-sync. "
				<< "Negative measured BHR can also be due to "
				<< "aborted-by-robots transactions.";
			descr << p2;

			XmlParagraph p3;
			p3 << XmlText("A less accurate way to measure hit ratio is to "
				"detect hits on the client-side using custom HTTP headers. "
				"A hit ratio table based on client-side tricks is available ");
			p3 << db.ptr("hit.ratio" + scope.oneSide("client"), XmlText("elsewhere"));
			p3 << XmlText(".");
			descr << p3;

			blob << descr;
		}

	} else {
		XmlParagraph para;
		para << XmlText(theOneSideWarn);
		if (cltSideExists()) {
			para << XmlText("  See ");
			para << db.ptr("summary" + theExecScope.oneSide("client"), XmlText("client-side"));
			para << XmlText(" information for hit ratio estimations (if any)");
		} else {
			para << XmlText("  No hit ratio measurements");
			para << XmlText(" can be derived from server-side logs.");
		}
		blob << para;
	}

	db << blob;
}

void TestInfo::cmplHitRatioTable(BlobDb &db, XmlTag &parent, const Scope &scope) {
	Assert(twoSided());

	static const String tlTitle = "hit ratio table";
	ReportBlob blob(BlobDb::Key("hit.ratio.table", scope), tlTitle);
	XmlTable table;
	table << XmlAttr::Int("border", 1) << XmlAttr::Int("cellspacing", 1);

	{
		XmlTableRec tr;
		tr << XmlTableHeading("Hit Ratios");

		XmlTableHeading dhr("DHR");
		dhr << XmlTag("br") << XmlText("(%)");
		tr << dhr;

		XmlTableHeading bhr("BHR");
		bhr << XmlTag("br") << XmlText("(%)");
		tr << bhr;

		table << tr;
	}

	{
		XmlTableRec tr;
		tr << algnLeft << XmlTableHeading("measured");

		XmlTableCell dhr;
		dhr << algnRight
			<< db.quote("hit.ratio.obj" + scope);
		tr << dhr;

		XmlTableCell bhr;
		bhr << algnRight
			<< db.quote("hit.ratio.byte" + scope);
		tr << bhr;

		table << tr;
	}

	blob << table;
	db << blob;
	parent << blob;
}

void TestInfo::cmplBaseStats(BlobDb &db, const Scope &scope) {
	static const String tlTitle = "baseline stats";
	ReportBlob blob(BlobDb::Key("baseline", scope), tlTitle);

	blob << db.quote(BlobDb::Key("load", scope));
	blob << db.quote(BlobDb::Key("hit.ratio", scope));

	db << blob;
}

void TestInfo::cmplTraffic(BlobDb &db, const Scope &scope) {
	static const String tlTitle = "test traffic stats";
	ReportBlob blob("traffic" + scope, tlTitle);

	XmlTag title("title");
	title << XmlText("Traffic rates, counts, and volumes");
	blob << title;

	blob << XmlTextTag<XmlParagraph>("This information is based on the client-side measurements.");

	blob << db.quote(BlobDb::Key("load", scope.oneSide("client")));
	blob << db.quote(BlobDb::Key("stream.table", scope.oneSide("client")));

	db << blob;
}

void TestInfo::cmplRptm(BlobDb &db, const Scope &scope) {
	static const String tlTitle = "test response time stats";
	ReportBlob blob("rptm" + scope, tlTitle);

	XmlTag title("title");
	title << XmlText("Response times");
	blob << title;

	blob << XmlTextTag<XmlParagraph>("This information is based on the client-side measurements.");

	blob << db.quote(BlobDb::Key("rptm.trace", scope.oneSide("client")));
	blob << db.quote("object.table" + scope.oneSide("client"));

	db << blob;
}

void TestInfo::cmplSavings(BlobDb &db, const Scope &scope) {
	static const String tlTitle = "cache effectiveness";
	ReportBlob blob("savings" + scope, tlTitle);

	XmlTag title("title");
	title << XmlText("Savings");
	blob << title;

	blob << db.quote("hit.ratio" + scope);

	db << blob;
}

void TestInfo::cmplLevels(BlobDb &db, const Scope &scope) {
	static const String tlTitle = "test transaction concurrency and population levels";
	ReportBlob blob("levels" + scope, tlTitle);

	XmlTag title("title");
	title << XmlText("Concurrency levels and robot population");
	blob << title;

	blob << XmlTextTag<XmlParagraph>("This information is based on the client-side measurements.");
	const InfoScope cltScope = scope.oneSide("client");

	{
		XmlSection s("concurrent HTTP/TCP connections");
		//s << db.quote("conn.level.fig" + cltScope);
		s << db.quote("conn.level.table" + cltScope);
		blob << s;
	}
		
	{
		XmlSection s("population level");
		//s << db.quote("populus.level.fig" + cltScope);
		s << db.quote("populus.level.table" + cltScope);
		blob << s;
	}
		
	{
		XmlSection s("concurrent HTTP transactions");
		//s << db.quote("xact.level.fig" + cltScope);
		s << db.quote("xact.level.table" + cltScope);
		blob << s;
	}
		
	db << blob;
}

void TestInfo::cmplErrors(BlobDb &db, const Scope &scope) {
	static const String tlTitle = "test errors";
	ReportBlob blob("errors" + scope, tlTitle);

	XmlTag title("title");
	title << XmlText("Errors");
	blob << title;

	{
		XmlSection s("client-side errors");
		s << db.include("errors.table" + scope.oneSide("client"));
		blob << s;
	}
		
	{
		XmlSection s("server-side errors");
		s << db.include("errors.table" + scope.oneSide("server"));
		blob << s;
	}
		
	db << blob;
}

void TestInfo::cmplNotes(BlobDb &db) {
	static const String tlTitle = "report notes";
	ReportBlob blob("report_notes", tlTitle);

	XmlSearchRes res;
	if (db.blobs().selByAttrName("report_note", res)) {
		XmlTag list("ul");
		for (int i = 0; i < res.count(); ++i)
			list << db.include(res[i]->attrs()->value("key"));
		blob << list;
	}

	db << blob;
}

void TestInfo::cmplSynonyms(BlobDb &db, const Scope &scope) {
	addLink(db, 
		BlobDb::Key("load", scope),
		BlobDb::Key("load", scope.oneSide("client")));
	addLink(db, 
		BlobDb::Key("load.table", scope),
		BlobDb::Key("load.table", scope.oneSide("client")));
	addLink(db,
		BlobDb::Key("stream.table", scope),
		BlobDb::Key("stream.table", scope.oneSide("client")));
}

void TestInfo::compileStats(BlobDb &db) {

	if (!theExecScope)
		guessExecScope();

	if (cltSideExists())
		cltSide().compileStats(db);
	else
		SideInfo::CompileEmptyStats(db, execScope().oneSide("client"));
	if (srvSideExists())
		srvSide().compileStats(db);
	else
		SideInfo::CompileEmptyStats(db, execScope().oneSide("server"));

	cmplExecSumVars(db);
	cmplExecSum(db);
	cmplWorkload(db);

	// build theScopes array
	theScopes.append(new Scope(execScope()));
	if (twoSided()) {
		Scope *allScope = new Scope;
		allScope->name("all phases");
		theScopes.append(allScope);
		for (int i = 0; i < cltSide().phaseCount(); ++i) {
			const String &pname = cltSide().phase(i).name();
			// include common phases only
			if (!srvSide().scope().hasPhase(pname))
				continue;
			theScopes.append(new Scope(theExecScope.onePhase(pname)));
			theScopes.last()->name(pname);
			allScope->add(*theScopes.last());
		}
	}

	for (int s = 0; s < theScopes.count(); ++s) {
		const Scope &scope = *theScopes[s];
		cmplSynonyms(db, scope);
		cmplHitRatioVars(db, scope);
		cmplHitRatio(db, scope);
		cmplBaseStats(db, scope);
		cmplTraffic(db, scope);
		cmplRptm(db, scope);
		cmplSavings(db, scope);
		cmplLevels(db, scope);
		cmplErrors(db, scope);
	}

	cmplNotes(db);
}


syntax highlighted by Code2HTML, v. 0.9.1