<?php
if(!class_exists('PHPUnit_Framework_TestResult', false)) require_once 'PHPUnit/Framework/TestResult.php';
if(!class_exists('PHPUnit_Framework_TestListener', false)) require_once 'PHPUnit/Framework/TestListener.php';

/**#@+
 * @var int
 */
/**
 * Failure test status constant
 */
define('TEST_FAILURE', -1);
/**
 * Error test status constant
 */
define('TEST_ERROR', 0);
/**
 * Success test status constant
 */
define('TEST_SUCCESS', 1);
/**
 * Incomplete test status constant
 */
define('TEST_INCOMPLETE', 2);
/**#@-*/

/**
 * Gathers details about PHPUnit2 test suites as they 
 * are been executed. This does not actually format any output
 * but simply gathers extended information about the overall 
 * results of all suites & their tests for use elsewhere.
 *
 * Changelog:
 *  0.6 First created [David Spurr]
 *  0.7 Added fix to getTestException provided [Glen Ogilvie]
 * 
 * @package framework
 * @subpackage testing
 *
 * @version 0.7 2006-03-12
 * @author David Spurr
 */
class SapphireTestReporter implements PHPUnit_Framework_TestListener {
	/**
	 * Holds array of suites and total number of tests run
	 * @var array
	 */	
	protected $suiteResults;
	/**
	 * Holds data of current suite that is been run
	 * @var array
	 */
	protected $currentSuite;
	/**
	 * Holds data of current test that is been run
	 * @var array
	 */
	protected $currentTest;
	/**
	 * Whether PEAR Benchmark_Timer is available for timing
	 * @var boolean
	 */
	protected $hasTimer;
	/**
	 * Holds the PEAR Benchmark_Timer object
	 * @var obj Benchmark_Timer
	 */
	protected $timer;
	
	protected $startTestTime;
	
	/**
	 * An array of all the test speeds
	 */
	protected $testSpeeds = array();
	
	/**
	 * Constructor, checks to see availability of PEAR Benchmark_Timer and
	 * sets up basic properties
	 * 
	 * @access public
	 * @return void
	 */
	public function __construct() {
  			@include_once 'Benchmark/Timer.php';
		if(class_exists('Benchmark_Timer')) {
   			$this->timer = new Benchmark_Timer();
   			$this->hasTimer = true;
		} else {
   			$this->hasTimer = false;
		}

   		$this->suiteResults = array(
   			'suites'      => array(),         // array of suites run
   			'hasTimer'    => $this->hasTimer, // availability of PEAR Benchmark_Timer
   			'totalTests'  => 0                // total number of tests run
   		);
	}
	
	/**
	 * Returns the suite results
	 * 
	 * @access public
	 * @return array Suite results
	 */
	public function getSuiteResults() {
		return $this->suiteResults;
	}
	
	/**
	 * Sets up the container for result details of the current test suite when
	 * each suite is first run
	 * 
	 * @access public
	 * @param obj PHPUnit2_Framework_TestSuite, the suite that is been run
	 * @return void
	 */
	public function startTestSuite( PHPUnit_Framework_TestSuite $suite) {
		if(strlen($suite->getName())) {
	    	$this->currentSuite = array(
	    		'suite'      => $suite,  // the test suite
	    		'tests'      => array(), // the tests in the suite
	    		'errors'     => 0,       // number of tests with errors
	    		'failures'   => 0,       // number of tests which failed
	    		'incomplete' => 0);    	 // number of tests that were not completed correctly
    	}
	}
	
	/**
	 * Sets up the container for result details of the current test when each 
	 * test is first run
	 * 
	 * @access public
	 * @param obj PHPUnit_Framework_Test, the test that is being run
	 * @return void
	 */
	public function startTest(PHPUnit_Framework_Test $test) {
		$this->startTestTime = microtime(true);
		
		if($test instanceof PHPUnit_Framework_TestCase) {
			$this->currentTest = array(
				'name'        => preg_replace('(\(.*\))', '', $test->toString()), // the name of the test (without the suite name)
				'timeElapsed' => 0,                // execution time of the test
				'status'      => TEST_SUCCESS,     // status of the test execution
				'message'     => '',               // user message of test result
				'exception'   => NULL,             // original caught exception thrown by test upon failure/error
				'uid'         => md5(microtime())  // a unique ID for this test (used for identification purposes in results)
			);
			if($this->hasTimer) $this->timer->start();
		}
	}
	
	/**
	 * Adds the failure detail to the current test and increases the failure
	 * count for the current suite
	 * 
	 * @access public
	 * @param obj PHPUnit_Framework_Test, current test that is being run
	 * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error
	 * @return void
	 */
	public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) {
		$this->currentSuite['failures']++;
		$this->currentTest['status'] = TEST_FAILURE;
		$this->currentTest['message'] = $e->toString();
		$this->currentTest['exception'] = $this->getTestException($test, $e);
		$this->currentTest['trace'] = $e->getTrace();
	}
	
	/**
	 * Adds the error detail to the current test and increases the error
	 * count for the current suite
	 * 
	 * @access public
	 * @param obj PHPUnit_Framework_Test, current test that is being run
	 * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error
	 * @return void
	 */
	public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) {
		$this->currentSuite['errors']++;
		$this->currentTest['status'] = TEST_ERROR;
		$this->currentTest['message'] = $e->getMessage();
		$this->currentTest['exception'] = $this->getTestException($test, $e);
		$this->currentTest['trace'] = $e->getTrace();
	}
	
	/**
	 * Adds the test incomplete detail to the current test and increases the incomplete
	 * count for the current suite
	 * 
	 * @access public
	 * @param obj PHPUnit_Framework_Test, current test that is being run
	 * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error
	 * @return void
	 */
	public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
		$this->currentSuite['incomplete']++;
		$this->currentTest['status'] = TEST_INCOMPLETE;
		$this->currentTest['message'] = $e->toString();
		$this->currentTest['exception'] = $this->getTestException($test, $e);
		$this->currentTest['trace'] = $e->getTrace();
	}
    
	/**
	 * Not used
	 *
	 * @param PHPUnit_Framework_Test $test
	 * @param unknown_type $time
	 */
	public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
		// not implemented
	}
	
    /**
	 * Upon completion of a test, records the execution time (if available) and adds the test to 
	 * the tests performed in the current suite.
	 * 
	 * @access public
	 * @param obj PHPUnit_Framework_Test, current test that is being run
	 * @return void
	 */
	public function endTest( PHPUnit_Framework_Test $test, $time) {
		$testDuration = microtime(true) - $this->startTestTime;
		$this->testSpeeds[$this->currentSuite['suite']->getName() . '.' . $this->currentTest['name']] = $testDuration;

		if($this->hasTimer) {
			$this->timer->stop();
			$this->currentTest['timeElapsed'] = $this->timer->timeElapsed();
		}
		array_push($this->currentSuite['tests'], $this->currentTest);
		if(method_exists($test, 'getActualOutput')) {
			$output = $test->getActualOutput();
			if($output) echo "\nOutput:\n$output";
		}
	}
	
	/**
	 * Upon completion of a test suite adds the suite to the suties performed
	 * 
	 * @access public
	 * @param obj PHPUnit_Framework_TestSuite, current suite that is being run
	 * @return void
	 */
	public function endTestSuite( PHPUnit_Framework_TestSuite $suite) {
		if(strlen($suite->getName())) {
			array_push($this->suiteResults['suites'], $this->currentSuite);
		}
	}
	
	/**
	 * Trys to get the original exception thrown by the test on failure/error 
	 * to enable us to give a bit more detail about the failure/error
	 * 
	 * @access private
	 * @param obj PHPUnit_Framework_Test, current test that is being run
	 * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error
	 * @return array
	 */
	private function getTestException(PHPUnit_Framework_Test $test, Exception $e) {
		// get the name of the testFile from the test
		$testName = preg_replace('/(.*)\((.*[^)])\)/', '\\2', $test->toString());
		$trace = $e->getTrace();
		// loop through the exception trace to find the original exception
		for($i = 0; $i < count($trace); $i++) {
			
			if(array_key_exists('file', $trace[$i])) {
				if(stristr($trace[$i]['file'], $testName.'.php') != false) return $trace[$i];
			}
			if(array_key_exists('file:protected', $trace[$i])) {
				if(stristr($trace[$i]['file:protected'], $testName.'.php') != false) return $trace[$i];
			}
		}
	}
	
	/**
	 * Display error bar if it exists
	 */
	public function writeResults() {
		$passCount = 0;
		$failCount = 0;
		$testCount = 0;
		$incompleteCount = 0;
		$errorCount = 0;
		
		foreach($this->suiteResults['suites'] as $suite) {
			foreach($suite['tests'] as $test) {
				$testCount++;
				if($test['status'] == 2) {
					$incompleteCount++;
				} elseif($test['status'] == 1) {
					$passCount++;
				} else {
					$failCount++;
				}

				if ($test['status'] != 1) {
					echo "<div class=\"failure\"><span>&otimes; ". $this->testNameToPhrase($test['name']) ."</span><br>";
					echo "<pre>".htmlentities($test['message'], ENT_COMPAT, 'UTF-8')."</pre><br>";
					echo SS_Backtrace::get_rendered_backtrace($test['trace']);
					echo "</div>";
				}
			}
		}
		$result = ($failCount > 0) ? 'fail' : 'pass';
		echo "<div class=\"status $result\">";
		echo "<h2><span>$testCount</span> tests run: <span>$passCount</span> passes, <span>$failCount</span> failures, and <span>$incompleteCount</span> incomplete</h2>";
		echo "</div>";
		
	}
	
	protected function testNameToPhrase($name) {
		return ucfirst(preg_replace("/([a-z])([A-Z])/", "$1 $2", $name));
	}
	
}