/**
 * This file is part of the KDE project
 *
 * Copyright (C) 2006 Nikolas Zimmermann <zimmermann@kde.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include <assert.h>
#include <signal.h>

#include <QtCore/QFile>
#include <QtCore/QTimer>
#include <QtCore/QProcess>
#include <QtCore/QFileInfo>
#include <QtCore/QTextStream>
#include <QtGui/QMainWindow>

#include <kiconloader.h>
#include <kmessagebox.h>
#include <kconfig.h>
#include <kfiledialog.h>

#include "test_regression_gui_window.moc"

// Taken from QUrl
#define Q_HAS_FLAG(a, b)	( ((a) & (b)) == (b) )
#define Q_SET_FLAG(a, b)	{ (a) |= (b); }
#define Q_UNSET_FLAG(a, b)	{ (a) &= ~(b); }

TestRegressionWindow::TestRegressionWindow(QWidget *parent)
: QMainWindow(parent), m_flags(None), m_runCounter(0), m_testCounter(0), m_totalTests(0),
					   m_totalTestsJS(0), m_totalTestsDOMTS(0), m_lastResult(Unknown),
					   m_browserPart(0), m_activeProcess(0), m_activeTreeItem(0),
					   m_suspended(false), m_justProcessingQueue(false)
{
	m_ui.setupUi(this);

	// Setup actions/connections
	connect(m_ui.actionOnly_run_JS_tests, SIGNAL(toggled(bool)), SLOT(toggleJSTests(bool)));
	connect(m_ui.actionOnly_run_HTML_tests, SIGNAL(toggled(bool)), SLOT(toggleHTMLTests(bool)));
	connect(m_ui.actionDo_not_suppress_debug_output, SIGNAL(toggled(bool)), SLOT(toggleDebugOutput(bool)));
	connect(m_ui.actionDo_not_use_Xvfb, SIGNAL(toggled(bool)), SLOT(toggleNoXvfbUse(bool)));
	connect(m_ui.actionSpecify_tests_directory, SIGNAL(triggered(bool)), SLOT(setTestsDirectory()));
	connect(m_ui.actionSpecify_khtml_directory, SIGNAL(triggered(bool)), SLOT(setKHTMLDirectory()));
	connect(m_ui.actionSpecify_output_directory, SIGNAL(triggered(bool)), SLOT(setOutputDirectory()));
	connect(m_ui.actionRun_tests, SIGNAL(triggered(bool)), SLOT(runTests()));

	connect(m_ui.pauseContinueButton, SIGNAL(clicked(bool)), SLOT(pauseContinueButtonClicked()));
	connect(m_ui.saveLogButton, SIGNAL(clicked(bool)), SLOT(saveLogButtonClicked()));

	connect(m_ui.treeWidget, SIGNAL(customContextMenuRequested(QPoint)),
			this, SLOT(treeWidgetContextMenuRequested(QPoint)));

	// Setup actions' default state
	m_ui.progressBar->setValue(0);
	m_ui.textEdit->setReadOnly(true);
	m_ui.actionRun_tests->setEnabled(false);
	m_ui.pauseContinueButton->setEnabled(false);

	m_ui.treeWidget->headerItem()->setTextAlignment(0, Qt::AlignLeft);
	m_ui.treeWidget->headerItem()->setText(0, i18n("Available Tests: 0"));

	// Load default values for tests directory/khtml directory...
	KConfig config("testregressiongui", KConfig::SimpleConfig);
	KConfigGroup grp = config.group("<default>");

	m_testsUrl = KUrl::fromPath(grp.readPathEntry("TestsDirectory", QString()));
	m_khtmlUrl = KUrl::fromPath(grp.readPathEntry("KHTMLDirectory", QString()));

	initTestsDirectory();

	// Init early visible items in the text edit...
	initLegend();
	initOutputBrowser();
}

TestRegressionWindow::~TestRegressionWindow()
{
	if(m_activeProcess)
	{
		m_activeProcess->kill();

		/* This leads to:
		 * QProcess object destroyed while process is still running.
		 * Any idea why??
		delete m_activeProcess;
		*/

		m_activeProcess = 0;
	}
}

void TestRegressionWindow::toggleJSTests(bool checked)
{
	if(checked)
	{
		Q_SET_FLAG(m_flags, JSTests)
		Q_UNSET_FLAG(m_flags, HTMLTests)

		m_ui.actionOnly_run_HTML_tests->setChecked(false);
	}
	else
		Q_UNSET_FLAG(m_flags, JSTests)

	// Eventually update progress bar range...
	updateProgressBarRange();
}

void TestRegressionWindow::toggleHTMLTests(bool checked)
{
	if(checked)
	{
		Q_SET_FLAG(m_flags, HTMLTests)
		Q_UNSET_FLAG(m_flags, JSTests)

		m_ui.actionOnly_run_JS_tests->setChecked(false);
	}
	else
		Q_UNSET_FLAG(m_flags, HTMLTests)

	// Eventually update progress bar range...
	updateProgressBarRange();
}

void TestRegressionWindow::toggleDebugOutput(bool checked)
{
	if(checked)
		Q_SET_FLAG(m_flags, DebugOutput)
	else
		Q_UNSET_FLAG(m_flags, DebugOutput)
}

void TestRegressionWindow::toggleNoXvfbUse(bool checked)
{
	if(checked)
		Q_SET_FLAG(m_flags, NoXvfbUse)
	else
		Q_UNSET_FLAG(m_flags, NoXvfbUse)
}

void TestRegressionWindow::setTestsDirectory()
{
	m_testsUrl = KFileDialog::getExistingDirectory();

	initTestsDirectory();
	loadOutputHTML();
}

void TestRegressionWindow::setOutputDirectory()
{
	m_outputUrl = KFileDialog::getExistingDirectory();
	loadOutputHTML();
}

void TestRegressionWindow::initTestsDirectory()
{
	bool okay = !m_testsUrl.isEmpty();
	if(okay)
	{
		const char *subdirs[] = { "tests", "baseline", "output", "resources" };
		for(int i = 0; i <= 3; i++)
		{
			QFileInfo sourceDir(m_testsUrl.path() + "/" + subdirs[i]);	//krazy:exclude=duoblequote_chars DOM demands chars
			if(!sourceDir.exists() || !sourceDir.isDir())
			{
				KMessageBox::error(0, i18n("Please choose a valid 'khtmltests/regression/' directory."));

				okay = false;
				m_testsUrl = KUrl();
				break;
			}
		}
	}

	if(okay)
	{
		// Clean up...
		m_itemMap.clear();
		m_ignoreMap.clear();
		m_failureMap.clear();
		m_directoryMap.clear();

		m_ui.treeWidget->clear();

		if(!m_khtmlUrl.isEmpty())
			m_ui.actionRun_tests->setEnabled(true);

		// Initialize map (to prevent assert below)...
		m_directoryMap.insert(QString(), QStringList());

		// Setup root tree widget item...
		(void) new QTreeWidgetItem(m_ui.treeWidget, QStringList(m_testsUrl.path() + "/tests"));

		// Check for ignore & failure file in root directory...
		QString ignoreFile = m_testsUrl.path() + "/tests/ignore";
		QString failureFile = m_testsUrl.path() + "/tests/KNOWN_FAILURES";

		QStringList ignoreFileList = readListFile(ignoreFile);
		QStringList failureFileList = readListFile(failureFile);

		if(!ignoreFileList.isEmpty())
			m_ignoreMap.insert(QString(), ignoreFileList);

		if(!failureFileList.isEmpty())
			m_failureMap.insert(QString(), failureFileList);

		// Remember directory...
		KConfig config("testregressiongui", KConfig::SimpleConfig);
		KConfigGroup grp = config.group("<default>");
		grp.writePathEntry("TestsDirectory", m_testsUrl.path());

		// Start listing directory...
		KUrl listUrl = m_testsUrl; listUrl.addPath("tests");
		KIO::ListJob *job = KIO::listRecursive(listUrl, KIO::HideProgressInfo, false /* no hidden files */);

		connect(job, SIGNAL(result(KJob*)), SLOT(directoryListingFinished(KJob*)));

		connect(job, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)),
				this, SLOT(directoryListingResult(KIO::Job*,KIO::UDSEntryList)));
	}
}

void TestRegressionWindow::setKHTMLDirectory()
{
	m_khtmlUrl = KFileDialog::getExistingDirectory();

	if(!m_khtmlUrl.isEmpty())
	{
		const char *subdirs[] = { "css", "dom", "xml", "html" }; // That's enough ;-)
		for(int i = 0; i <= 3; i++)
		{
			QFileInfo sourceDir(m_khtmlUrl.path() + "/" + subdirs[i]);	//krazy:exclude=duoblequote_chars DOM demands chars
			if(!sourceDir.exists() || !sourceDir.isDir())
			{
				KMessageBox::error(0, i18n("Please choose a valid 'khtml/' build directory."));

				m_khtmlUrl = KUrl();
				break;
			}
		}

		// Remember directory...
		KConfig config("testregressiongui", KConfig::SimpleConfig);
		KConfigGroup grp = config.group("<default>");
		grp.writePathEntry("KHTMLDirectory", m_khtmlUrl.path());

		if(!m_testsUrl.isEmpty() && !m_khtmlUrl.isEmpty())
			m_ui.actionRun_tests->setEnabled(true);
	}
}

void TestRegressionWindow::directoryListingResult(KIO::Job *, const KIO::UDSEntryList &list)
{
	KIO::UDSEntryList::ConstIterator it = list.constBegin();
	const KIO::UDSEntryList::ConstIterator end = list.constEnd();

	for(; it != end; ++it)
	{
		const KIO::UDSEntry &entry = *it;

		QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
		if(entry.isDir()) // Create new map entry...
		{
			assert(m_directoryMap.constFind(name) == m_directoryMap.constEnd());
			m_directoryMap.insert(name, QStringList());

			QString ignoreFile = m_testsUrl.path() + "/tests/" + name + "/ignore";
			QString failureFile = m_testsUrl.path() + "/tests/" + name + "/KNOWN_FAILURES";

			QStringList ignoreFileList = readListFile(ignoreFile);
			QStringList failureFileList = readListFile(failureFile);

			if(!ignoreFileList.isEmpty())
				m_ignoreMap.insert(name, ignoreFileList);

			if(!failureFileList.isEmpty())
				m_failureMap.insert(name, failureFileList);
		}
		else if(name.endsWith(".html") || name.endsWith(".htm") ||
				name.endsWith(".xhtml") || name.endsWith(".xml") || name.endsWith(".js"))
		{
			int lastSlashPos = name.lastIndexOf('/');

			QString cachedDirectory = (lastSlashPos > 0 ? name.mid(0, lastSlashPos) : QString());
			QString cachedFilename = name.mid(lastSlashPos + 1);

			assert(m_directoryMap.constFind(cachedDirectory) != m_directoryMap.constEnd());
			m_directoryMap[cachedDirectory].append(cachedFilename);
		}
	}
}

void TestRegressionWindow::directoryListingFinished(KJob *)
{
	QTreeWidgetItem *topLevelItem = m_ui.treeWidget->topLevelItem(0);

	// Gather a lot of statistics...
	unsigned long availableDomFiles = 0;
	unsigned long availableDumpFiles = 0;
	unsigned long availableRenderFiles = 0;

	unsigned long ignoredJSTests = 0;
	unsigned long availableJSTests = 0;

	unsigned long ignoredXMLTests = 0;
	unsigned long availableXMLTests = 0;

	unsigned long ignoredHTMLTests = 0;
	unsigned long availableHTMLTests = 0;

	unsigned long ignoredDOMTSTests = 0;
	unsigned long availableDOMTSTests = 0;

	// Start the actual data processing...
	QMap<QString, QStringList>::const_iterator it = m_directoryMap.constBegin();
	const QMap<QString, QStringList>::const_iterator end = m_directoryMap.constEnd();

	for(; it != end; ++it)
	{
		QString directory = it.key();
		QStringList filenames = it.value();

		if(filenames.isEmpty()) // Do not add empty directories at all...
			continue;

		bool hasIgnores = (m_ignoreMap.constFind(directory) != m_directoryMap.constEnd());
		bool hasFailures = (m_failureMap.constFind(directory) != m_failureMap.constEnd());

		// Extract parent directory...
		int position = directory.lastIndexOf('/');

		QString parentDirectory = directory.mid(0, (position == -1 ? 0 : position));
		QString parentDirectoryItem = directory.mid(position + 1);

		bool hasParentIgnores = (m_ignoreMap.constFind(parentDirectory) != m_directoryMap.constEnd());
		bool hasParentFailures = (m_failureMap.constFind(parentDirectory) != m_failureMap.constEnd());

		// Sort in ascending order...
		filenames.sort();

		QStringList::const_iterator it2 = filenames.constBegin();
		const QStringList::const_iterator end2 = filenames.constEnd();

		// Create new tree widget item for the active directory...
		QTreeWidgetItem *parent = topLevelItem;

		if(!directory.isEmpty())
		{
			parent = new QTreeWidgetItem(topLevelItem, QStringList(directory));

			// Directory is completely ignored, mark it 'yellow'...
			if(hasParentIgnores && m_ignoreMap[parentDirectory].contains(parentDirectoryItem))
				parent->setIcon(0, m_ignorePixmap);

			// Directory is completely known to fail, mark it 'red'...
			if(hasParentFailures && m_failureMap[parentDirectory].contains(parentDirectoryItem))
				parent->setIcon(0, m_failKnownPixmap);
		}

		// Add all contained files as new items below 'parent'...
		for(; it2 != end2; ++it2)
		{
			QString test = (*it2);
			QString cacheName = directory + "/" + test;	//krazy:exclude=duoblequote_chars DOM demands chars

			QTreeWidgetItem *testItem = new QTreeWidgetItem(parent, QStringList(KUrl(test).path()));

			// Remember name <-> item pair...
			assert(m_itemMap.contains(cacheName));
			m_itemMap.insert(cacheName, testItem);

			bool ignore = (hasIgnores && m_ignoreMap[directory].contains(test));
			bool ignoreParent = (hasParentIgnores && m_ignoreMap[parentDirectory].contains(parentDirectoryItem));

			bool failure = (hasFailures && m_failureMap[directory].contains(test));

			// Check baseline directory for this test...
			QString baseLinePath = m_testsUrl.path() + "/baseline/" + cacheName;

			bool dom[9], render[9];
			for(unsigned int i = 0; i < 9; ++i)
			{
				if(i == 0)
				{
					dom[i] = (QFileInfo(baseLinePath + "-dom").exists());
					render[i] = (QFileInfo(baseLinePath + "-render").exists());
				}
				else
				{
					dom[i] = (QFileInfo(baseLinePath + "-" + QString::number(i) + "-dom").exists());	//krazy:exclude=duoblequote_chars DOM demands chars
					render[i] = (QFileInfo(baseLinePath + "-" + QString::number(i) + "-render").exists());	//krazy:exclude=duoblequote_chars DOM demands chars
				}
			}

			bool dump = (QFileInfo(baseLinePath + "-dump.png").exists());

			// Ignored tests are marked 'yellow'...
			if(ignore)
				testItem->setIcon(0, m_ignorePixmap);

			// Tests, known to fail, are marked 'red'...
			if(failure)
				testItem->setIcon(0, m_failKnownPixmap);

			// Detect whether the tests has no corresponding baseline items...
			if(!ignore && !failure)
			{
				if(!dom[0] && !dump && !render && !cacheName.endsWith(".js") && !cacheName.startsWith("domts"))
				{
					// See if parent directory is completely ignored...
					if(!ignoreParent)
						testItem->setIcon(0, m_noBaselinePixmap);
				}
			}

			// Update statistics...
			if(dump)
				availableDumpFiles++;

			for(unsigned i = 0; i < 9; ++i)
			{
				if(dom[i])
					availableDomFiles++;

				if(render[i])
					availableRenderFiles++;
			}

			// Count DOM Testsuite files separated... (these have no baseline items!)
			if(cacheName.startsWith("domts"))
			{
				// See if parent directory is completely ignored...
				if(ignore || ignoreParent)
					ignoredDOMTSTests++;
				else
					availableDOMTSTests++;
			}

			if(cacheName.endsWith(".html") || cacheName.endsWith(".htm") || cacheName.endsWith(".xhtml"))
			{
				if(ignore || ignoreParent)
					ignoredHTMLTests++;
				else
					availableHTMLTests++;
			}
			else if(cacheName.endsWith(".xml"))
			{
				if(ignore || ignoreParent)
					ignoredXMLTests++;
				else
					availableXMLTests++;
			}
			else if(cacheName.endsWith(".js"))
			{
				unsigned long containedTests = 0;

				// Try hard to _ESTIMATE_ the number of tests...
				// I really meant estimate, no way to calculate it perfectly.

				QString jsFilePath = m_testsUrl.path() + "/tests/" + cacheName;
				assert(QFileInfo(jsFilePath).exists() == true);

				QStringList fileList = readListFile(jsFilePath);
				QString fileContent = fileList.join("");

				// #1 -> Check js file for the 'reportResult' calls...
				containedTests = fileContent.count("reportResult");

				// #2 -> Check js file for 'openPage' calls...
				containedTests += fileContent.count("openPage");

				// #3 -> Check js file for 'checkOutput' calls...
				containedTests += fileContent.count("checkOutput");

				// #4 -> Fallback for ie. mozilla/ecma files...
				if(containedTests == 0) // Doesn't use 'reportResult' scheme...
					containedTests++;

				if(ignore || ignoreParent)
					ignoredJSTests += containedTests;
				else
					availableJSTests += containedTests;
			}
		}
	}

	// Now we can calculate all ignored/available tests...
	unsigned long ignoredTests = ignoredJSTests + ignoredXMLTests + ignoredHTMLTests;
	unsigned long availableTests = availableJSTests + availableXMLTests + availableHTMLTests;

	// This estimates the number of total tests, depending on the mode...
	m_totalTests = availableDomFiles + availableDumpFiles + availableRenderFiles +
				   availableDOMTSTests + availableJSTests;

	m_totalTestsJS = availableJSTests;
	m_totalTestsDOMTS = availableDOMTSTests;

	// Update progress bar range...
	updateProgressBarRange();

	QString statistics = QString("<body><table border='0' align='center' cellspacing='15'>") +
						 QString("<tr valign='top'><td colspan='3'><center><b>Statistics</b></center></td></tr>") +
						 QString("<tr valign='middle'><td>JS Tests</td><td>" + QString::number(availableJSTests) + "</td><td>(" + QString::number(ignoredJSTests) + " ignored)</td></tr>") +
						 QString("<tr valign='middle'><td>XML Tests</td><td>" + QString::number(availableXMLTests) + "</td><td>(" + QString::number(ignoredXMLTests) + " ignored)</td></tr>") +
						 QString("<tr valign='middle'><td>HTML Tests</td><td>" + QString::number(availableHTMLTests) + "</td><td>(" + QString::number(ignoredHTMLTests) + " ignored)</td></tr>") +
						 QString("</table></body>");

	// Go to end...
	QTextCursor cursor = m_ui.textEdit->textCursor();
	cursor.movePosition(QTextCursor::End);
	m_ui.textEdit->setTextCursor(cursor);

	// Insert statistics...
	m_ui.textEdit->insertHtml(statistics);

	// Update treeview...
	m_ui.treeWidget->headerItem()->setText(0, i18n("Available Tests: %1 (ignored: %2)", availableTests, ignoredTests));
}

void TestRegressionWindow::updateProgressBarRange() const
{
	if(m_totalTests != 0 && m_totalTestsJS != 0)
	{
		unsigned long totalTests = m_totalTests;

		if(Q_HAS_FLAG(m_flags, JSTests))
			totalTests = m_totalTestsJS;
		else if(Q_HAS_FLAG(m_flags, HTMLTests))
		{
			totalTests -= m_totalTestsJS;
			totalTests -= m_totalTestsDOMTS;
		}

		m_ui.progressBar->setRange(0, totalTests);
	}
}

void TestRegressionWindow::pauseContinueButtonClicked()
{
	assert(m_activeProcess != 0);

	if(!m_suspended)
	{
		// Suspend process
		kill(m_activeProcess->pid(), SIGSTOP);

		m_suspended = true;
		m_ui.pauseContinueButton->setText(i18n("Continue"));
	}
	else
	{
		// Continue process
		kill(m_activeProcess->pid(), SIGCONT);

		m_suspended = false;
		m_ui.pauseContinueButton->setText(i18n("Pause"));
	}
}

void TestRegressionWindow::saveLogButtonClicked()
{
	assert(m_activeProcess == 0);
	m_saveLogUrl = KFileDialog::getExistingDirectory();

	QString fileName = m_saveLogUrl.path() + "/logOutput.html";
	if(QFileInfo(fileName).exists())
	{
		// Remove file if already existent...
		QFile file(fileName);
		if(!file.remove())
		{
			kError() << " Can't remove " << fileName << endl;
			exit(1);
		}
	}
}

void TestRegressionWindow::runTests()
{
	// Run in all-in-one mode...
	m_runCounter = 0;
	m_testCounter = 0;

	initRegressionTesting(QString());
}

void TestRegressionWindow::runSingleTest()
{
	assert(m_activeTreeItem != 0);

	QString testFileName = pathFromItem(m_activeTreeItem);

	// Run in single-test mode...
	m_runCounter = 0;
	m_testCounter = -1;

	initRegressionTesting(testFileName);
}

void TestRegressionWindow::initRegressionTesting(const QString &testFileName)
{
	assert(m_activeProcess == 0);

	m_activeProcess = new QProcess();
	m_activeProcess->setReadChannelMode(QProcess::MergedChannels);

	QStringList environment = QProcess::systemEnvironment();
	environment << "KDE_DEBUG=false"; // No Dr. Konqi please!

	QString program = m_khtmlUrl.path() + "/.libs/testregression";
	QString program2 = m_khtmlUrl.path() + "/testregression"; // with CMake, it's in $buildir/bin

	if(!QFileInfo(program).exists())
	{
		if(!QFileInfo(program2).exists())
		{
			KMessageBox::error(0, i18n("Cannot find testregression executable."));
			return;
		}
		else
		{
			program = program2;
		}
	}

	QStringList arguments;
	arguments << "--base" << m_testsUrl.path();

	if(!m_outputUrl.isEmpty())
		arguments << "--output" << m_outputUrl.path();
	if(!testFileName.isEmpty())
		arguments << "--test" << testFileName;

	if(Q_HAS_FLAG(m_flags, JSTests))
		arguments << "--js";
	if(Q_HAS_FLAG(m_flags, HTMLTests))
		arguments << "--html";
	if(Q_HAS_FLAG(m_flags, DebugOutput))
		arguments << "--debug";
	if(Q_HAS_FLAG(m_flags, NoXvfbUse))
		arguments << "--noxvfb";

	connect(m_activeProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(testerExited(int,QProcess::ExitStatus)));
	connect(m_activeProcess, SIGNAL(readyReadStandardOutput()), SLOT(testerReceivedData()));

	// Clear processing queue before starting...
	m_processingQueue.clear();

	// Clean up gui...
	m_ui.textEdit->clear();
	m_ui.progressBar->reset();
	m_ui.saveLogButton->setEnabled(false);
	m_ui.actionRun_tests->setEnabled(false);
	m_ui.pauseContinueButton->setEnabled(true);

	// Start regression testing process...
	m_activeProcess->setEnvironment(environment);
	m_activeProcess->start(program, arguments, QIODevice::ReadOnly);
}

void TestRegressionWindow::initOutputBrowser()
{
	assert(m_browserPart == 0);
	m_browserPart = new KHTMLPart(m_ui.secondTab, m_ui.secondTab, KHTMLPart::BrowserViewGUI);

	// Setup vertical layout for the browser widget...
	QVBoxLayout *layout = new QVBoxLayout();
	layout->addWidget(m_browserPart->widget());
	m_ui.secondTab->setLayout(layout);

	m_browserPart->setJavaEnabled(true);
	m_browserPart->setJScriptEnabled(true);
	m_browserPart->setPluginsEnabled(true);
	m_browserPart->setURLCursor(QCursor(Qt::PointingHandCursor));

	m_browserPart->widget()->show();

	// Check if there is already an output/index.html present...
	loadOutputHTML();
}

void TestRegressionWindow::loadOutputHTML() const
{
	if(m_testsUrl.isEmpty())
		return;

	QString fileName = m_testsUrl.path() + "/output/index.html";
	if(!m_outputUrl.isEmpty())
		fileName = m_outputUrl.path() + "/index.html";

	QFileInfo indexHtml(fileName);
	if(indexHtml.exists())
	{
		m_browserPart->openUrl(KUrl::fromPath(fileName));
		m_ui.tabWidget->setTabEnabled(1, true);
	}
	else
		m_ui.tabWidget->setTabEnabled(1, false);
}

void TestRegressionWindow::updateItemStatus(TestResult result, QTreeWidgetItem *item, const QString &testFileName)
{
	if(!item)
		return;

	// Ensure item is visible...
	QTreeWidgetItem *parent = item;
	while(parent != 0)
	{
		m_ui.treeWidget->setItemExpanded(parent, true);
		parent = parent->parent();
	}

	m_ui.treeWidget->scrollToItem(item);

	bool updateIcon = true;
	if(m_lastName == testFileName && !m_lastName.isEmpty())
	{
		if(m_lastResult == result)
			updateIcon = false;
		else if((m_lastResult == Pass || m_lastResult == PassUnexpected) &&
				(result == Fail || result == FailKnown || result == Crash))
		{
			// If one part of the test (render/dom/paint) passed,
			// and the current part fails, update to 'failed' icon...
			updateIcon = true;
		}
		else if((m_lastResult == Fail || m_lastResult == FailKnown || m_lastResult == Crash) &&
				(result == Pass || result == PassUnexpected))
		{
			// If one part of the test (render/dom/paint) failed,
			// and the current part passes, don't update to 'passed' icon...
			updateIcon = false;
		}
	}

	// Update icon, if necessary...
	if(updateIcon)
	{
		if(result == Crash)
			item->setIcon(0, m_crashPixmap);
		else if(result == Fail)
			item->setIcon(0, m_failPixmap);
		else if(result == FailKnown)
			item->setIcon(0, m_failKnownPixmap);
		else if(result == Pass)
			item->setIcon(0, m_passPixmap);
		else if(result == PassUnexpected)
			item->setIcon(0, m_passUnexpectedPixmap);
		else // Unhandled state...
			assert(false);
	}

	// Remember test & result...
	m_lastResult = result;
	m_lastName = testFileName;
	m_activeTreeItem = item;
}

void TestRegressionWindow::initLegend()
{
	// Init pixmaps...
	m_failPixmap = QPixmap(":/test/pics/fail.xpm");
	m_failKnownPixmap = QPixmap(":/test/pics/failKnown.xpm");
	m_passPixmap = QPixmap(":/test/pics/pass.xpm");
	m_passUnexpectedPixmap = QPixmap(":/test/pics/passUnexpected.xpm");
	m_ignorePixmap = QPixmap(":/test/pics/ignore.xpm");
	m_crashPixmap = QPixmap(":/test/pics/crash.xpm");
	m_noBaselinePixmap = QPixmap(":/test/pics/noBaseline.xpm");

	QString legend = QLatin1String("<body><center><font size='8'>Welcome to the khtml<br/>") +
					 QLatin1String("regression testing tool!</font></center><br/><br/>") +
					 QLatin1String("<table border='0' align='center' cellspacing='15'>") +
					 QLatin1String("<tr valign='top'><td colspan='2'><center><b>Legend</b></center></td></tr>") +
					 QLatin1String("<tr valign='middle'><td>Pass</td><td><img src=':/test/pics/pass.xpm'></td></tr>") +
					 QLatin1String("<tr valign='middle'><td>Pass unexpected</td><td><img src=':/test/pics/passUnexpected.xpm'></td></tr>") +
					 QLatin1String("<tr valign='middle'><td>Fail</td><td><img src=':/test/pics/fail.xpm'></td></tr>") +
					 QLatin1String("<tr valign='middle'><td>Fail known</td><td><img src=':/test/pics/failKnown.xpm'></td></tr>") +
					 QLatin1String("<tr valign='middle'><td>Ignore</td><td><img src=':/test/pics/ignore.xpm'></td></tr>") +
					 QLatin1String("<tr valign='middle'><td>Baseline missing</td><td><img src=':/test/pics/noBaseline.xpm'></td></tr>") +
					 QLatin1String("<tr valign='middle'><td>Crash</td><td><img src=':/test/pics/crash.xpm'></td></tr>") +
					 QLatin1String("</table></body>");

	m_ui.textEdit->setHtml(legend);
}

void TestRegressionWindow::testerExited(int /* exitCode */, QProcess::ExitStatus exitStatus)
{
	assert(m_activeProcess != 0);
	assert(m_activeTreeItem != 0);

	if(exitStatus == QProcess::CrashExit) // Special case: crash!
	{
		QTreeWidgetItem *useItem = m_activeTreeItem;

		if(m_testCounter >= 0 || m_runCounter > 0) // Single-tests mode invoked on a directory OR All-test-mode
		{
			QTreeWidgetItem *parent = useItem->parent();
			assert(parent != 0);

			useItem = parent->child(parent->indexOfChild(useItem) + 1);
			assert(useItem != 0);
		}

		// Reflect crashed test...
		updateItemStatus(Crash, useItem, QString());
	}

	if(m_testCounter >= 0) // All-tests mode
		m_ui.progressBar->setValue(m_ui.progressBar->maximum());

	// Eventually save log output...
	if(!m_saveLogUrl.isEmpty())
	{
		// We should close our written log with </body></html>.
		m_processingQueue.enqueue(QString::fromLatin1("\n</body>\n</html>"));

		if(!m_justProcessingQueue)
		{
			m_justProcessingQueue = true;
			QTimer::singleShot(50, this, SLOT(processQueue()));
		}
	}

	// Cleanup gui...
	m_ui.saveLogButton->setEnabled(true);
	m_ui.actionRun_tests->setEnabled(true);
	m_ui.pauseContinueButton->setEnabled(false);

	// Check if there is already an output/index.html present...
	loadOutputHTML();

	// Cleanup data..
	delete m_activeProcess;
	m_activeProcess = 0;

	m_runCounter = 0;
	m_testCounter = 0;
	m_activeTreeItem = 0;
}

void TestRegressionWindow::testerReceivedData()
{
	assert(m_activeProcess != 0);

	QString data(m_activeProcess->readAllStandardOutput());
	QStringList list = data.split('\n');

	QStringList::const_iterator it = list.constBegin();
	const QStringList::const_iterator end = list.constEnd();

	for(; it != end; ++it)
	{
		QString temp = *it;
		if(!temp.isEmpty())
			m_processingQueue.enqueue(temp);
	}

	if(!m_justProcessingQueue)
	{
		m_justProcessingQueue = true;
		QTimer::singleShot(50, this, SLOT(processQueue()));
	}
}

void TestRegressionWindow::processQueue()
{
	while(!m_processingQueue.isEmpty())
	{
		QString data = m_processingQueue.dequeue();
		TestResult result = Unknown;

		QString cacheName = extractTestNameFromData(data, result);

		if(result != Unknown) // Yes, we're dealing with a test result...
		{
			if(cacheName.isEmpty()) // Make sure everything is alright!
			{
				kError() << "Couldn't extract cacheName from data=\"" << data << "\"! Ignoring!" << endl;
				continue;
			}
		}

		parseRegressionTestingOutput(data, result, cacheName);
	}

	m_justProcessingQueue = false;
}

void TestRegressionWindow::addToIgnores()
{
	assert(m_activeTreeItem != 0);

	QString treeItemText = pathFromItem(m_activeTreeItem);

	// Extract directory/file name...
	int position = treeItemText.lastIndexOf('/');

	QString directory = treeItemText.mid(0, (position == -1 ? 0 : position));
	QString fileName = treeItemText.mid(position + 1);

	// Read corresponding ignore file..
	QString ignoreFile = m_testsUrl.path() + "/tests/" + directory + "/ignore";
	QStringList ignoreFileList = readListFile(ignoreFile);

	if(!ignoreFileList.contains(fileName))
		ignoreFileList.append(fileName);

	// Commit changes...
	writeListFile(ignoreFile, ignoreFileList);

	// Reset icon status...
	m_activeTreeItem->setIcon(0, m_ignorePixmap);
}

void TestRegressionWindow::removeFromIgnores()
{
	assert(m_activeTreeItem != 0);

	QString treeItemText = pathFromItem(m_activeTreeItem);

	// Extract directory/file name...
	int position = treeItemText.lastIndexOf('/');

	QString directory = treeItemText.mid(0, (position == -1 ? 0 : position));
	QString fileName = treeItemText.mid(position + 1);

	// Read corresponding ignore file..
	QString ignoreFile = m_testsUrl.path() + "/tests/" + directory + "/ignore";
	QStringList ignoreFileList = readListFile(ignoreFile);

	if(ignoreFileList.contains(fileName))
		ignoreFileList.removeAll(fileName);

	// Commit changes...
	writeListFile(ignoreFile, ignoreFileList);

	// Reset icon status...
	m_activeTreeItem->setIcon(0, QPixmap());
}

QString TestRegressionWindow::pathFromItem(const QTreeWidgetItem *item) const
{
	QString path = item->text(0);

	QTreeWidgetItem *parent = item->parent();
	while(parent != 0)
	{
		if(parent->parent() != 0)
			path.prepend(parent->text(0) + "/");	//krazy:exclude=duoblequote_chars DOM demands chars

		parent = parent->parent();
	}

	return path;
}

QString TestRegressionWindow::extractTestNameFromData(QString &data, TestResult &result) const
{
	if(data.indexOf("PASS") >= 0 || data.indexOf("FAIL") >= 0)
	{
		// Name extraction regexps...
		QString bracesSelector("[0-9a-zA-Z-_<>\\* +-,.:!?$'\"=/\\[\\]\\(\\)]*");

		QRegExp expPass("PASS: (" + bracesSelector + ")");	//krazy:exclude=duoblequote_chars DOM demands chars
		QRegExp expPassUnexpected("PASS \\(unexpected!\\): (" + bracesSelector + ")");	//krazy:exclude=duoblequote_chars DOM demands chars

		QRegExp expFail("FAIL: (" + bracesSelector + ")");	//krazy:exclude=duoblequote_chars DOM demands chars
		QRegExp expFailKnown("FAIL \\(known\\): (" + bracesSelector + ")");	//krazy:exclude=duoblequote_chars DOM demands chars

		// Extract name of test... (while using regexps as rare as possible!)
		int pos = -1;
		QString test;

		QRegExp cleanTest(" \\[" + bracesSelector + "\\]");

		pos = expPass.indexIn(data);
		if(pos > -1) { test = expPass.cap(1); result = Pass; }

		if(result == Unknown)
		{
			pos = expPassUnexpected.indexIn(data);
			if(pos > -1) { test = expPassUnexpected.cap(1); result = PassUnexpected; }
		}

		if(result == Unknown)
		{
			pos = expFail.indexIn(data);
			if(pos > -1) { test = expFail.cap(1); result = Fail; }
		}

		if(result == Unknown)
		{
			pos = expFailKnown.indexIn(data);
			if(pos > -1) { test = expFailKnown.cap(1); result = FailKnown; }
		}

		if(!test.isEmpty() && result != Unknown) // Got information about test...
		{
			// Clean up first, so we only get the file name...
			test.replace(cleanTest, QString());

			// Extract cached directory/filename pair...
			int lastSlashPos = test.lastIndexOf('/');

			QString cachedDirectory = (lastSlashPos > 0 ? test.mid(0, lastSlashPos) : QString());
			QString cachedFilename = test.mid(lastSlashPos + 1);

			if(cachedDirectory == ".") // Handle cases like "./empty.html"
				cachedDirectory.clear();

			assert(m_directoryMap.constFind(cachedDirectory) != m_directoryMap.constEnd());

			QString cacheName = cachedDirectory + "/" + cachedFilename;	//krazy:exclude=duoblequote_chars DOM demands chars
			if(m_itemMap.constFind(cacheName) != m_itemMap.constEnd())
			{
				// Highlight test...
				data.replace(expPass, "<b><font color='green'>PASS:\t\\1</font></b>");
				data.replace(expPassUnexpected, "<b><font color='green'>PASS (unexpected!):\t\\1</font></b>");
				data.replace(expFail, "<b><font color='red'>FAIL:\t\\1</font></b>");
				data.replace(expFailKnown, "<b><font color='red'>FAIL (known):\t\\1</font></b>");

				return cacheName;
			}
		}
	}

	return QString();
}

void TestRegressionWindow::parseRegressionTestingOutput(QString data, TestResult result, const QString &cacheName)
{
	if(!cacheName.isEmpty())
	{
		if(m_testCounter >= 0) // Only increment in all-tests mode...
			m_testCounter++;

		m_runCounter++; // Always increment...

		// Update the icon...
		updateItemStatus(result, m_itemMap[cacheName], cacheName);
	}

	// Apply some nice formatting for the statistics...
	if(data.indexOf("Total") >= 0 || data.indexOf("Passes") >= 0 || data.indexOf("Tests completed") >= 0 ||
	   data.indexOf("Errors:") >= 0 || data.indexOf("Failures:") >= 0)
	{
		QRegExp expTotal("Total:    ([0-9]*)");
		QRegExp expPasses("Passes:   ([0-9 a-z\\(\\)]*)");
		QRegExp expErrors("Errors:   ([0-9 a-z]*)");
		QRegExp expFailures("Failures: ([0-9 a-z\\(\\)]*)");

		data.replace("Tests completed.", "<br><center><h2>Tests completed.</h2></center>");
		data.replace(expTotal, "<b><font size='4'>Total:&nbsp;\\1</font></b>");
		data.replace(expPasses, "<b><font size='4' color='green'>Passes:&nbsp;\\1</font></b>");
		data.replace(expErrors, "<b><font size='4' color='blue'>Errors:&nbsp;\\1</font></b>");
		data.replace(expFailures, "<b><font size='4' color='red'>Failures:&nbsp;\\1</font></b>");
	}

	if(!data.contains("</body>\n</html>")) // Don't put <br> behind </html>!
		data.append("<br>");

	// Update text edit...
	updateLogOutput(data);

	// Update progressbar...
	if(m_testCounter > 0)
		m_ui.progressBar->setValue(m_testCounter);
}

void TestRegressionWindow::treeWidgetContextMenuRequested(const QPoint &pos)
{
	if((m_testCounter == -1 && m_activeProcess) || (m_testCounter > 0 && m_activeProcess) ||
	   m_testsUrl.isEmpty() || m_khtmlUrl.isEmpty()) // Still processing/not ready yet...
	{
		return;
	}

	QTreeWidgetItem *item = m_ui.treeWidget->itemAt(pos);
	if(item && item != m_ui.treeWidget->topLevelItem(0))
	{
		m_activeTreeItem = item;

		// Build & show popup menu...
		QMenu menu(m_ui.treeWidget);

		menu.addAction(SmallIcon("media-playback-start"), i18n("Run test..."), this, SLOT(runSingleTest()));
		menu.addSeparator();
		menu.addAction(SmallIcon("list-add"), i18n("Add to ignores..."), this, SLOT(addToIgnores()));
		menu.addAction(SmallIcon("dialog-cancel"), i18n("Remove from ignores..."), this, SLOT(removeFromIgnores()));

		if(!menu.exec(m_ui.treeWidget->mapToGlobal(pos)))
			m_activeTreeItem = 0; // Needs reset...
	}
}

void TestRegressionWindow::updateLogOutput(const QString &data)
{
	QTextCursor cursor = m_ui.textEdit->textCursor();

	// Append 'data'...
	m_ui.textEdit->insertHtml(data);

	// Keep a maximum of 100 lines in the log...
	const int maxLogLines = 100;

	long logLines = countLogLines();
	if(logLines > maxLogLines)
	{
		cursor.movePosition(QTextCursor::Start);
		cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, logLines - maxLogLines);
		cursor.removeSelectedText();
	}

	// Go to end...
	cursor.movePosition(QTextCursor::End);
	m_ui.textEdit->setTextCursor(cursor);

	// Eventually save log output...
	if(!m_saveLogUrl.isEmpty())
	{
		QString fileName = m_saveLogUrl.path() + "/logOutput.html";
		QIODevice::OpenMode fileFlags = QIODevice::WriteOnly;

		bool fileExists = QFileInfo(fileName).exists();
		if(fileExists)
			fileFlags |= QIODevice::Append;

		QFile file(fileName);
		if(!file.open(fileFlags))
		{
			kError() << " Can't open " << fileName << endl;
			exit(1);
		}

		if(!fileExists)
			file.write(QString::fromLatin1("<html>\n<body>\n").toLatin1());

		file.write(QString(data + "\n").toLatin1());	//krazy:exclude=duoblequote_chars DOM demands chars
		file.close();

		// Reset save log url, if we reached the end...
		if(data.contains("</body>\n</html>"))
			m_saveLogUrl = KUrl();
	}
}

unsigned long TestRegressionWindow::countLogLines() const
{
	QTextCursor cursor = m_ui.textEdit->textCursor();
	cursor.movePosition(QTextCursor::Start);

	unsigned long lines = 0;
	while(cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor))
		lines++;

	return lines;
}

QStringList TestRegressionWindow::readListFile(const QString &fileName) const
{
	QStringList files;

	QFileInfo fileInfo(fileName);
	if(fileInfo.exists())
	{
		QFile file(fileName);
		if(!file.open(QIODevice::ReadOnly))
		{
			kError() << " Can't open " << fileName << endl;
			exit(1);
		}

		QString line;

		QTextStream fileStream(&file);
		while(!(line = fileStream.readLine()).isNull())
			files.append(line);

		file.close();
	}

	return files;
}

void TestRegressionWindow::writeListFile(const QString &fileName, const QStringList &content) const
{
	QFile file(fileName);
	if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
	{
		kError() << " Can't open " << fileName << endl;
		exit(1);
	}

	file.write(content.join("\n").toLatin1());
	file.close();
}

// vim:ts=4:tw=4:noet
