<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 * @version $Id$
 */
require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';

/**
 * Base class for Integration tests.
 *
 * Provides helpers to track data and then call API get* methods to check outputs automatically.
 *
 */
abstract class IntegrationTestCase extends DatabaseTestCase
{

    /**
     * Identifies the last language used in an API/Controller call.
     *
     * @var string
     */
    protected $lastLanguage;

    /**
     * Initializes the test
     * Load english translations to ensure API response have english text
     *
     * @see tests/core/Test_Database#setUp()
     */
    public function setUp()
    {
        parent::setUp();

        if (self::$widgetTestingLevel != self::NO_WIDGET_TESTING)
        {
            self::initializeControllerTesting();
        }

        Piwik::createAccessObject();
        Piwik_PostEvent('FrontController.initAuthenticationObject');

        // We need to be SU to create websites for tests
        Piwik::setUserIsSuperUser();

        // Load and install plugins
        $pluginsManager = Piwik_PluginsManager::getInstance();
        $plugins = Piwik_Config::getInstance()->Plugins['Plugins'];

        $pluginsManager->loadPlugins( $plugins );
        $pluginsManager->installLoadedPlugins();

        $_GET = $_REQUEST = array();
        $_SERVER['HTTP_REFERER'] = '';

        // Make sure translations are loaded to check messages in English
        Piwik_Translate::getInstance()->loadEnglishTranslation();

        // List of Modules, or Module.Method that should not be called as part of the XML output compare
        // Usually these modules either return random changing data, or are already tested in specific unit tests.
        $this->setApiNotToCall(self::$defaultApiNotToCall);
        $this->setApiToCall( array());

        if (self::$widgetTestingLevel != self::NO_WIDGET_TESTING)
        {
            Piwik::setUserIsSuperUser();

            // create users for controller testing
            $usersApi = Piwik_UsersManager_API::getInstance();
            $usersApi->addUser('anonymous', self::DEFAULT_USER_PASSWORD, 'anonymous@anonymous.com');
            $usersApi->addUser('test_view', self::DEFAULT_USER_PASSWORD, 'view@view.com');
            $usersApi->addUser('test_admin', self::DEFAULT_USER_PASSWORD, 'admin@admin.com');

            // disable shuffling of tag cloud visualization so output is consistent
            Piwik_Visualization_Cloud::$debugDisableShuffle = true;
        }

        $this->setUpWebsitesAndGoals();
        $this->trackVisits();
    }

    abstract protected function setUpWebsitesAndGoals();

    abstract protected function trackVisits();

    public function tearDown()
    {
        parent::tearDown();
        $_GET = $_REQUEST = array();
        Piwik_Translate::getInstance()->unloadEnglishTranslation();

        // re-enable tag cloud shuffling
        Piwik_Visualization_Cloud::$debugDisableShuffle = true;
    }

    protected $apiToCall = array();
    protected $apiNotToCall = array();

    public static $defaultApiNotToCall = array(
        'LanguagesManager',
        'DBStats',
        'UsersManager',
        'SitesManager',
        'ExampleUI',
        'Live',
        'SEO',
        'ExampleAPI',
        'PDFReports',
        'API',
        'ImageGraph',
    );

    /**
     * Widget testing level constant. If self::$widgetTestingLevel is
     * set to this, controller actions will not be tested.
     */
    const NO_WIDGET_TESTING = 'none';

    /**
     * Widget testing level constant. If self::$widgetTestingLevel is
     * set to this, controller actions will be checked for non-fatal errors, but
     * the output will be ignored.
     */
    const CHECK_WIDGET_ERRORS = 'check_errors';

    /**
     * Widget testing level constant. If self::$widgetTestingLevel is
     * set to this, controller actions will be run & their output will be checked with
     * expected output files.
     */
    const COMPARE_WIDGET_OUTPUT = 'compare_output';

    /**
     * Determines how much of controller actions are tested (if at all).
     */
    static public $widgetTestingLevel = self::NO_WIDGET_TESTING;

    /**
     * API testing level constant. If self::$apiTestingLevel is
     * set to this, API methods will not be tested.
     */
    const NO_API_TESTING = 'none';

    /**
     * API testing level constant. If self::$apiTestingLevel is
     * set to this, API methods will be run & their output will be checked with
     * expected output files.
     */
    const COMPARE_API_OUTPUT = 'compare_output';

    /**
     * Determines how much testing API methods are subjected to (if any).
     */
    static public $apiTestingLevel = self::COMPARE_API_OUTPUT;

    const DEFAULT_USER_PASSWORD = 'nopass';

    /**
     * Forces the test to only call and fetch XML for the specified plugins,
     * or exact API methods.
     * If not called, all default tests will be executed.
     *
     * @param array $apiToCall array( 'ExampleAPI', 'Plugin.getData' )
     *
     * @throws Exception
     * @return void
     */
    protected function setApiToCall( $apiToCall )
    {
        if(func_num_args() != 1)
        {
            throw new Exception('setApiToCall expects an array');
        }
        if(!is_array($apiToCall))
        {
            $apiToCall = array($apiToCall);
        }
        $this->apiToCall = $apiToCall;
    }

    /**
     * Sets a list of API methods to not call during the test
     *
     * @param string $apiNotToCall eg. 'ExampleAPI.getPiwikVersion'
     *
     * @return void
     */
    protected function setApiNotToCall( $apiNotToCall )
    {
        if(!is_array($apiNotToCall))
        {
            $apiNotToCall = array($apiNotToCall);
        }
        $this->apiNotToCall = $apiNotToCall;
    }

    /**
     * Returns a PiwikTracker object that you can then use to track pages or goals.
     *
     * @param         $idSite
     * @param         $dateTime
     * @param boolean $defaultInit If set to true, the tracker object will have default IP, user agent, time, resolution, etc.
     *
     * @return PiwikTracker
     */
    protected function getTracker($idSite, $dateTime, $defaultInit = true )
    {
        $t = new PiwikTracker( $idSite, $this->getTrackerUrl());
        $t->setForceVisitDateTime($dateTime);

        if($defaultInit)
        {
            $t->setIp('156.5.3.2');

            // Optional tracking
            $t->setUserAgent( "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729)");
            $t->setBrowserLanguage('fr');
            $t->setLocalTime( '12:34:06' );
            $t->setResolution( 1024, 768 );
            $t->setBrowserHasCookies(true);
            $t->setPlugins($flash = true, $java = true, $director = false);
        }
        return $t;
    }

    /**
     * Creates a website, then sets its creation date to a day earlier than specified dateTime
     * Useful to create a website now, but force data to be archived back in the past.
     *
     * @param string  $dateTime eg '2010-01-01 12:34:56'
     * @param int     $ecommerce
     * @param string  $siteName
     *
     * @return int    idSite of website created
     */
    protected function createWebsite( $dateTime, $ecommerce = 0, $siteName = 'Piwik test' )
    {
        $idSite = Piwik_SitesManager_API::getInstance()->addSite(
            $siteName,
            "http://piwik.net/",
            $ecommerce,
            $ips = null,
            $excludedQueryParameters = null,
            $timezone = null,
            $currency = null
        );

        // Manually set the website creation date to a day earlier than the earliest day we record stats for
        Zend_Registry::get('db')->update(Piwik_Common::prefixTable("site"),
            array('ts_created' => Piwik_Date::factory($dateTime)->subDay(1)->getDatetime()),
            "idsite = $idSite"
        );

        // Clear the memory Website cache
        Piwik_Site::clearCache();

        // add access to all test users if doing controller tests
        if (self::$widgetTestingLevel != self::NO_WIDGET_TESTING)
        {
            $usersApi = Piwik_UsersManager_API::getInstance();
            $usersApi->setUserAccess('anonymous', 'view', array($idSite));
            $usersApi->setUserAccess('test_view', 'view', array($idSite));
            $usersApi->setUserAccess('test_admin', 'admin', array($idSite));
        }

        return $idSite;
    }

    /**
     * Checks that the response is a GIF image as expected.
     * Will fail the test if the response is not the expected GIF
     *
     * @param $response
     */
    protected function checkResponse($response)
    {
        $trans_gif_64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
        $expectedResponse = base64_decode($trans_gif_64);
        $this->assertEquals($expectedResponse, $response, "Expected GIF beacon, got: <br/>\n" . $response ."<br/>\n");
    }

    /**
     * Returns URL to the proxy script, used to ensure piwik.php
     * uses the test environment, and allows variable overwriting
     *
     * @return string
     */
    protected function getTrackerUrl()
    {
        $piwikUrl = Piwik_Url::getCurrentUrlWithoutFileName();

        $pathBeforeRoot = 'tests';
        // Running from a plugin
        if(strpos($piwikUrl, 'plugins/') !== false)
        {
            $pathBeforeRoot = 'plugins';
        }
        $piwikUrl = substr($piwikUrl, 0, strpos($piwikUrl, $pathBeforeRoot.'/')) . 'tests/PHPUnit/proxy-piwik.php';
        return $piwikUrl;
    }

    /**
     * Initializes parts of Piwik so controller actions can be called & tested.
     */
    public static function initializeControllerTesting()
    {
        static $initialized = false;

        if (!$initialized)
        {
            Zend_Registry::set('timer', new Piwik_Timer);

            $pluginsManager = Piwik_PluginsManager::getInstance();
            $pluginsToLoad = Piwik_Config::getInstance()->Plugins['Plugins'];
            $pluginsManager->loadPlugins( $pluginsToLoad );

            $initialized = true;
        }
    }

    public static function processRequestArgs()
    {
        // set the widget testing level
        if (isset($_GET['widgetTestingLevel']))
        {
            self::setWidgetTestingLevel($_GET['widgetTestingLevel']);
        }

        // set the API testing level
        if (isset($_GET['apiTestingLevel']))
        {
            self::setApiTestingLevel($_GET['apiTestingLevel']);
        }
    }

    public static function setWidgetTestingLevel($level)
    {
        if (!$level) return;

        if ($level != self::NO_WIDGET_TESTING &&
            $level != self::CHECK_WIDGET_ERRORS &&
            $level != self::COMPARE_WIDGET_OUTPUT)
        {
            echo "<p>Invalid option for 'widgetTestingLevel', ignoring.</p>\n";
            return;
        }

        self::$widgetTestingLevel = $level;
    }

    public function setApiTestingLevel($level)
    {
        if (!$level) return;

        if ($level != self::NO_API_TESTING &&
            $level != self::COMPARE_API_OUTPUT)
        {
            echo "<p>Invalid option for 'apiTestingLevel', ignoring.</p>";
            return;
        }

        self::$apiTestingLevel = $level;
    }

    /**
     * Given a list of default parameters to set, returns the URLs of APIs to call
     * If any API was specified in setApiToCall() we ensure only these are tested.
     * If any API is set as excluded (see list below) then it will be ignored.
     *
     * @param array       $parametersToSet Parameters to set in api call
     * @param array       $formats         Array of 'format' to fetch from API
     * @param array       $periods         Array of 'period' to query API
     * @param bool        $setDateLastN    If set to true, the 'date' parameter will be rewritten to query instead a range of dates, rather than one period only.
     * @param bool|string $language        2 letter language code, defaults to default piwik language
     * @param bool|string $segment
     *
     * @return array of API URLs query strings
     */
    protected function generateUrlsApi( $parametersToSet, $formats, $periods, $setDateLastN = false, $language = false, $segment = false )
    {
        // Get the URLs to query against the API for all functions starting with get*
        $skipped = $requestUrls = array();
        $apiMetadata = new Piwik_API_DocumentationGenerator;
        foreach(Piwik_API_Proxy::getInstance()->getMetadata() as $class => $info)
        {
            $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class);
            foreach($info as $methodName => $infoMethod)
            {
                $apiId = $moduleName.'.'.$methodName;

                // If Api to test were set, we only test these
                if(!empty($this->apiToCall)
                    && in_array($moduleName, $this->apiToCall) === false
                    && in_array($apiId, $this->apiToCall) === false)
                {
                    $skipped[] = $apiId;
                    continue;
                }
                // Excluded modules from test
                elseif(
                    (strpos($methodName, 'get') !== 0
                        || in_array($moduleName, $this->apiNotToCall) === true
                        || in_array($apiId, $this->apiNotToCall) === true
                        || $methodName == 'getLogoUrl'
                        || $methodName == 'getHeaderLogoUrl'
                    )
                )
                {
                    $skipped[] = $apiId;
                    continue;
                }

                foreach($periods as $period)
                {
                    $parametersToSet['period'] = $period;

                    // If date must be a date range, we process this date range by adding 6 periods to it
                    if($setDateLastN)
                    {
                        if(!isset($parametersToSet['dateRewriteBackup']))
                        {
                            $parametersToSet['dateRewriteBackup'] = $parametersToSet['date'];
                        }

                        $lastCount = (int)$setDateLastN;
                        if($setDateLastN === true)
                        {
                            $lastCount = 6;
                        }
                        $firstDate = $parametersToSet['dateRewriteBackup'];
                        $secondDate = date('Y-m-d', strtotime("+$lastCount " . $period . "s", strtotime($firstDate)));
                        $parametersToSet['date'] = $firstDate . ',' . $secondDate;
                    }

                    // Set response language
                    if($language !== false)
                    {
                        $parametersToSet['language'] = $language;
                    }
                    // Generate for each specified format
                    foreach($formats as $format)
                    {
                        $parametersToSet['format'] = $format;
                        $parametersToSet['hideIdSubDatable'] = 1;
                        $parametersToSet['serialize'] = 1;

                        $exampleUrl = $apiMetadata->getExampleUrl($class, $methodName, $parametersToSet);
                        if($exampleUrl === false)
                        {
                            $skipped[] = $apiId;
                            continue;
                        }

                        // Remove the first ? in the query string
                        $exampleUrl = substr($exampleUrl, 1);
                        $apiRequestId = $apiId;
                        if(strpos($exampleUrl, 'period=') !== false)
                        {
                            $apiRequestId .= '_' . $period;
                        }

                        $apiRequestId .= '.' . $format;

                        $requestUrls[$apiRequestId] = $exampleUrl;
                    }
                }
            }
        }
        return $requestUrls;
    }

    /**
     * Will call all get* methods on authorized modules,
     * force the archiving,
     * record output in XML files
     * and compare with the expected outputs.
     *
     * @param string            $testName       Used to write the output in a file, used as filename prefix
     * @param string|array      $formats        String or array of formats to fetch from API
     * @param int|bool          $idSite         Id site
     * @param string|bool       $dateTime       Date time string of reports to request
     * @param array|bool|string $periods        String or array of strings of periods (day, week, month, year)
     * @param bool              $setDateLastN   When set to true, 'date' parameter passed to API request will be rewritten to query a range of dates rather than 1 date only
     * @param string|bool       $language       2 letter language code to request data in
     * @param string|bool       $segment        Custom Segment to query the data  for
     * @param string|bool       $visitorId      Only used for Live! API testing
     * @param bool              $abandonedCarts Only used in Goals API testing
     * @param bool              $idGoal
     * @param bool              $apiModule
     * @param bool              $apiAction
     * @param array             $otherRequestParameters
     *
     * @return void
     */
    protected function _callGetApiCompareOutput($testName, $formats = 'xml', $idSite = false, $dateTime = false, $periods = false,
                                     $setDateLastN = false, $language = false, $segment = false, $visitorId = false, $abandonedCarts = false,
                                     $idGoal = false, $apiModule = false, $apiAction = false, $otherRequestParameters = array())
    {
        if (self::$apiTestingLevel == self::NO_API_TESTING)
        {
            return;
        }

        list($pathProcessed, $pathExpected) = $this->getProcessedAndExpectedDirs();

        if($periods === false)
        {
            $periods = 'day';
        }
        if(!is_array($periods))
        {
            $periods = array($periods);
        }
        if(!is_array($formats))
        {
            $formats = array($formats);
        }
        if(!is_writable($pathProcessed))
        {
            $this->fail('To run the tests, you need to give write permissions to the following directory (create it if it doesn\'t exist).<code><br/>mkdir '. $pathProcessed.'<br/>chmod 777 '.$pathProcessed.'</code><br/>');
        }
        $parametersToSet = array(
            'idSite'           => $idSite,
            'date'             => $periods == array('range') ? $dateTime : date('Y-m-d', strtotime($dateTime)),
            'expanded'         => '1',
            'piwikUrl'         => 'http://example.org/piwik/',
            // Used in getKeywordsForPageUrl
            'url'              => 'http://example.org/store/purchase.htm',

            // Used in Actions.getPageUrl, .getDownload, etc.
            // tied to Main.test.php doTest_oneVisitorTwoVisits
            // will need refactoring when these same API functions are tested in a new function
            'downloadUrl'      => urlencode('http://piwik.org/path/again/latest.zip?phpsessid=this is ignored when searching'),
            'outlinkUrl'       => urlencode('http://dev.piwik.org/svn'),
            'pageUrl'          => urlencode('http://example.org/index.htm?sessionid=this is also ignored by default'),
            'pageName'         => urlencode(' Checkout / Purchasing... '),

            // do not show the millisec timer in response or tests would always fail as value is changing
            'showTimer'        => 0,

            'language'         => $language ? $language : 'en',
            'abandonedCarts'   => $abandonedCarts ? 1 : 0,
            'idSites'          => $idSite,
        );
        $parametersToSet = array_merge($parametersToSet, $otherRequestParameters);
        if(!empty($visitorId ))
        {
            $parametersToSet['visitorId'] = $visitorId;
        }
        if(!empty($apiModule ))
        {
            $parametersToSet['apiModule'] = $apiModule;
        }
        if(!empty($apiAction))
        {
            $parametersToSet['apiAction'] = $apiAction;
        }
        if(!empty($segment))
        {
            $parametersToSet['segment'] = $segment;
        }
        if($idGoal !== false)
        {
            $parametersToSet['idGoal'] = $idGoal;
        }

        $requestUrls = $this->generateUrlsApi($parametersToSet, $formats, $periods, $setDateLastN, $language, $segment);

        foreach($requestUrls as $apiId => $requestUrl)
        {
            #echo "\n\n$requestUrl\n\n";
            $isLiveMustDeleteDates = strpos($requestUrl, 'Live.getLastVisits') !== false;
            $request = new Piwik_API_Request($requestUrl);

            list($processedFilePath, $expectedFilePath) = $this->getProcessedAndExpectedPaths($testName, $apiId);

            // Cast as string is important. For example when calling
            // with format=original, objects or php arrays can be returned.
            // we also hide errors to prevent the 'headers already sent' in the ResponseBuilder (which sends Excel headers multiple times eg.)
            $response = (string)$request->process();

            if($isLiveMustDeleteDates)
            {
                $response = $this->removeAllLiveDatesFromXml($response);
            }

            file_put_contents( $processedFilePath, $response );

            $expected = $this->loadExpectedFile($expectedFilePath);
            if (empty($expected))
            {
                continue;
            }

            // @todo This should not vary between systems AFAIK... "idsubdatatable can differ"
            $expected = $this->removeXmlElement($expected, 'idsubdatatable',$testNotSmallAfter = false);
            $response = $this->removeXmlElement($response, 'idsubdatatable',$testNotSmallAfter = false);

            if($isLiveMustDeleteDates)
            {
                $expected = $this->removeAllLiveDatesFromXml($expected);
            }
            // If date=lastN the <prettyDate> element will change each day, we remove XML element before comparison
            elseif(strpos($dateTime, 'last') !== false
                || strpos($dateTime, 'today') !== false
                || strpos($dateTime, 'now') !== false
            )
            {
                if(strpos($requestUrl, 'API.getProcessedReport') !== false)
                {
                    $expected = $this->removePrettyDateFromXml($expected);
                    $response = $this->removePrettyDateFromXml($response);
                }
                // avoid build failure when running just before midnight, generating visits in the future
                $expected = $this->removeXmlElement($expected, 'sum_daily_nb_uniq_visitors');
                $response = $this->removeXmlElement($response, 'sum_daily_nb_uniq_visitors');
                $expected = $this->removeXmlElement($expected, 'nb_visits_converted');
                $response = $this->removeXmlElement($response, 'nb_visits_converted');
                $expected = $this->removeXmlElement($expected, 'imageGraphUrl');
                $response = $this->removeXmlElement($response, 'imageGraphUrl');
            }

            // is there a better way to test for the current DB type in use?
            if(Zend_Registry::get('db') instanceof Piwik_Db_Adapter_Mysqli)
            {
                // Do not test for TRUNCATE(SUM()) returning .00 on mysqli since this is not working
                // http://bugs.php.net/bug.php?id=54508
                $expected = str_replace('.00</revenue>', '</revenue>', $expected);
                $response = str_replace('.00</revenue>', '</revenue>', $response);
                $expected = str_replace('.1</revenue>', '</revenue>', $expected);
                $expected = str_replace('.11</revenue>', '</revenue>', $expected);
                $response = str_replace('.11</revenue>', '</revenue>', $response);
                $response = str_replace('.1</revenue>', '</revenue>', $response);
            }

            if(strpos($requestUrl, 'format=xml') !== false) {
                $this->assertXmlStringEqualsXmlString($expected, $response, "Differences with expected in: $processedFilePath %s ");
            } else {
                $this->assertEquals($expected, $response, "Differences with expected in: $processedFilePath %s ");
            }
            if(trim($response) == trim($expected))
            {
                file_put_contents( $processedFilePath, $response );
            }
        }
    }

    /**
     * Calls a set of controller actions & either checks the result against
     * expected output or just checks if errors occurred when called.
     * The behavior of this function can be modified by setting
     * self::$widgetTestingLevel (or $testingLevelOverride):
     * <ul>
     *   <li>If set to <b>NO_WIDGET_TESTING</b> this function simply returns.<li>
     *   <li>If set to <b>CHECK_WIDGET_ERRORS</b> controller actions are called &
     *       this function will just check for errors.</li>
     *   <li>If set to <b>COMPARE_WIDGET_OUTPUT</b> controller actions are
     *       called & the output is checked against expected output.</li>
     * </ul>
     *
     * @param string $testName             Unique name of this test group. Expected/processed
     *                                     file names use this as a prefix.
     * @param array  $actions              Array of controller actions to call. Each element
     *                                     must be in the following format: 'Controller.action'
     * @param array  $requestParameters    The request parameters to set.
     * @param array  $userTypes            The user types to test the controller with. Can contain
     *                                     these values: 'anonymous', 'view', 'admin', 'superuser'.
     *                                     Defaults to all four.
     * @param int    $testingLevelOverride Overrides self::$widgetTestingLevel.
     */
    public function callWidgetsCompareOutput(
        $testName, $actions, $requestParameters, $userTypes = null, $testingLevelOverride = null)
    {
        // deal with the testing level
        if (self::$widgetTestingLevel == self::NO_WIDGET_TESTING)
        {
            return;
        }

        if (is_null($testingLevelOverride))
        {
            $testingLevelOverride = self::$widgetTestingLevel;
        }

        // process $userTypes argument
        if (!$userTypes)
        {
            $userTypes = array('anonymous', 'view', 'admin', 'superuser');
        }
        else if (!is_array($userTypes))
        {
            $userTypes = array($userTypes);
        }

        $oldGet = $_GET;

        // get all testable controller actions if necessary
        $actionParams = array();
        if ($actions == 'all')
        {
            // Goals.addWidgets requires idSite to be set
            $_GET['idSite'] = isset($requestParameters['idSite']) ? $requestParameters['idSite'] : '0';

            list($actions, $actionParams) = $this->findAllWidgets();

            $_GET = $oldGet;
        }
        else if (!is_array($actions))
        {
            $actions = array($actions);
        }

        // run the tests
        foreach ($actions as $controllerAction)
        {
            $customParams = isset($actionParams[$controllerAction]) ? $actionParams[$controllerAction] : array();
            list($controllerName, $actionName) = explode('.', $controllerAction);

            foreach ($userTypes as $userType)
            {
                $this->setUserType($userType);

                try
                {
                    // set request parameters
                    $_GET = array();
                    foreach ($customParams as $key => $value)
                    {
                        $_GET[$key] = $value;
                    }
                    foreach ($requestParameters as $key => $value)
                    {
                        $_GET[$key] = $value;
                    }

                    $_GET['module'] = $controllerName;
                    $_GET['action'] = $actionName;

                    if ($testingLevelOverride == self::CHECK_WIDGET_ERRORS)
                    {
                        $this->errorsOccurredInTest = array();
                        set_error_handler(array($this, "customErrorHandler"));
                    }

                    // call controller action
                    $response = Piwik_FrontController::getInstance()->fetchDispatch();

                    list($processedFilePath, $expectedFilePath) = $this->getProcessedAndExpectedPaths(
                        $testName . '_' . $userType, $controllerAction, 'html');

                    if ($testingLevelOverride == self::CHECK_WIDGET_ERRORS)
                    {
                        restore_error_handler();

                        if (!empty($this->errorsOccurredInTest))
                        {
                            // write processed (only if there are errors)
                            file_put_contents($processedFilePath, $response);

                            $this->fail("PHP Errors occurred in calling controller action '$controllerAction':");
                            foreach ($this->errorsOccurredInTest as $error)
                            {
                                echo "&nbsp;   $error<br/>\n";
                            }
                        }
                    }
                    else // check against expected
                    {
                        // write raw processed response
                        file_put_contents($processedFilePath, $response);

                        // load expected
                        $expected = $this->loadExpectedFile($expectedFilePath);
                        if (!$expected)
                        {
                            continue;
                        }

                        // normalize eol delimeters
                        $expected = str_replace("\r\n", "\n", $expected);
                        $response = str_replace("\r\n", "\n", $response);

                        // check against expected
                        $passed = $this->assertEquals(trim($expected), trim($response),
                            "<br/>\nDifferences with expected in: $processedFilePath %s ");

                        if (!$passed)
                        {
                            var_dump('ERROR FOR ' . $controllerAction . ' -- FETCHED RESPONSE, then EXPECTED RESPONSE - ');
                            echo "<br/>\n";
                            var_dump(htmlspecialchars($response));
                            echo "<br/>\n";
                            var_dump(htmlspecialchars($expected));
                            echo "<br/>\n";
                        }
                    }
                }
                catch (Exception $e)
                {
                    $this->fail("EXCEPTION THROWN IN $controllerAction: ".$e->getTraceAsString());
                }
            }
        }

        // reset $_GET to old values
        $_GET = array();
        foreach ($oldGet as $key => $value)
        {
            $_GET[$key] = $value;
        }

        // set user type
        $this->setUserType('superuser');
    }

    /**
     * Sets the access privilegs of the current user to the specified user type.
     *
     * @param $userType string Can be 'superuser', 'admin', 'view' or 'anonymous'.
     */
    protected function setUserType( $userType )
    {
        if ($userType == 'superuser')
        {
            $code = Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE;
            $login = 'superUserLogin';
        }
        else
        {
            $code = 0;

            $login = $userType;
            if ($login != 'anonymous')
            {
                $login = 'test_' . $login;
            }
        }

        $authResultObj = new Piwik_Auth_Result($code, $login, 'dummyTokenAuth');
        $authObj = new MockPiwik_Auth();
        $authObj->setReturnValue('getName', 'Login');
        $authObj->setReturnValue('authenticate', $authResultObj);

        Zend_Registry::get('access')->reloadAccess($authObj);
    }

    /**
     * Set of messages for errors that occurred during the invocation of a
     * controller action. If not empty, there was an error in the controller.
     */
    private $errorsOccurredInTest = array();

    /**
     * A custom error handler used with <code>set_error_handler</code>. If
     * an error occurs, a message describing it is saved in an array.
     *
     * @param int    $errno
     * @param string $errstr
     * @param string $errfile
     * @param int    $errline
     *
     * @return void
     */
    public function customErrorHandler($errno, $errstr, $errfile, $errline)
    {
        if (strpos(strtolower($errstr), 'cannot modify header information - headers already sent')) // HACK
        {
            $this->errorsOccurredInTest[] = "$errfile($errline): - $errstr";
        }
    }

    /**
     * Returns a list of all available widgets.
     */
    protected function findAllWidgets()
    {
        $widgetList = Piwik_GetWidgetsList();

        $actions = array();
        $customParams = array();

        foreach($widgetList as $widgetCategory => $widgets)
        {
            foreach($widgets as $widgetInfo)
            {
                $module = $widgetInfo['parameters']['module'];
                $moduleAction = $widgetInfo['parameters']['action'];
                $wholeAction = "$module.$moduleAction";

                // FIXME: can't test Referers.getKeywordsForPage since it tries to make a request to
                // localhost w/ the wrong url. Piwik_Url::getCurrentUrlWithoutFileName
                // returns /tests/integration/?... when used within a test.
                if ($wholeAction == "Referers.getKeywordsForPage")
                {
                    continue;
                }

                // rss widgets depends on feedburner URL. don't test the widget just in case
                // feedburner is down.
                if ($module == "ExampleRssWidget"
                    || $module == "ExampleFeedburner")
                {
                    continue;
                }

                unset($widgetInfo['parameters']['module']);
                unset($widgetInfo['parameters']['action']);

                $actions[] = $wholeAction;
                $customParams[$wholeAction] = $widgetInfo['parameters'];
            }
        }

        return array($actions, $customParams);
    }

    protected function removeAllLiveDatesFromXml($input)
    {
        $toRemove = array(
            'serverDate',
            'firstActionTimestamp',
            'lastActionTimestamp',
            'lastActionDateTime',
            'serverTimestamp',
            'serverTimePretty',
            'serverDatePretty',
            'serverDatePrettyFirstAction',
            'serverTimePrettyFirstAction',
            'goalTimePretty',
            'serverTimePretty',
            'visitorId'
        );
        foreach($toRemove as $xml) {
            $input = $this->removeXmlElement($input, $xml);
        }
        return $input;
    }

    protected function removePrettyDateFromXml($input)
    {
        return $this->removeXmlElement($input, 'prettyDate');
    }

    protected function removeXmlElement($input, $xmlElement, $testNotSmallAfter = true)
    {
        $input = preg_replace('/(<'.$xmlElement.'>.+?<\/'.$xmlElement.'>)/', '', $input);
        //check we didn't delete the whole string
        if($testNotSmallAfter)
        {
            $this->assertTrue(strlen($input) > 100);
        }
        return $input;
    }

    private function getProcessedAndExpectedDirs()
    {
        $path = $this->getPathToTestDirectory();
        return array($path . '/processed/', $path . '/expected/');
    }

    private function getProcessedAndExpectedPaths($testName, $testId, $format = null)
    {
        $filename = $testName . '__' . $testId;
        if ($format)
        {
            $filename .= ".$format";
        }

        list($processedDir, $expectedDir) = $this->getProcessedAndExpectedDirs();

        return array($processedDir . $filename, $expectedDir . $filename);
    }

    private function loadExpectedFile($filePath)
    {
        $result = @file_get_contents($filePath);
        if(empty($result))
        {
            $expectedDir = dirname($filePath);
            $this->fail(" ERROR: Could not find expected API output '$filePath'. For new tests, to pass the test, you can copy files from the processed/ directory into $expectedDir  after checking that the output is valid. %s ");
            return null;
        }
        return $result;
    }







    /**
     * Returns an array describing the API methods to call & compare with
     * expected output.
     *
     * The returned array must be of the following format:
     * <code>
     * array(
     *     array('SomeAPI.method', array('testOption1' => 'value1', 'testOption2' => 'value2'),
     *     array(array('SomeAPI.method', 'SomeOtherAPI.method'), array(...)),
     *     .
     *     .
     *     .
     * )
     * </code>
     *
     * Valid test options:
     * <ul>
     *   <li><b>testSuffix</b> The suffix added to the test name. Helps determine
     *   the filename of the expected output.</li>
     *   <li><b>format</b> The desired format of the output. Defaults to 'xml'.</li>
     *   <li><b>idSite</b> The id of the website to get data for.</li>
     *   <li><b>date</b> The date to get data for.</li>
     *   <li><b>periods</b> The period or periods to get data for. Can be an array.</li>
     *   <li><b>setDateLastN</b> Flag describing whether to query for a set of
     *   dates or not.</li>
     *   <li><b>language</b> The language to use.</li>
     *   <li><b>segment</b> The segment to use.</li>
     *   <li><b>visitorId</b> The visitor ID to use.</li>
     *   <li><b>abandonedCarts</b> Whether to look for abandoned carts or not.</li>
     *   <li><b>idGoal</b> The goal ID to use.</li>
     *   <li><b>apiModule</b> The value to use in the apiModule request parameter.</li>
     *   <li><b>apiAction</b> The value to use in the apiAction request parameter.</li>
     *   <li><b>otherRequestParameters</b> An array of extra request parameters to use.</li>
     *   <li><b>disableArchiving</b> Disable archiving before running tests.</li>
     * </ul>
     *
     * All test options are optional, except 'idSite' & 'date'.
     */
    public function getApiForTesting() {
        return array();
    }

    /**
     * Returns an array describing the Controller actions to call & compare
     * with expected output.
     *
     * The returned array must be of the following format:
     * <code>
     * array(
     *     array('Controller.action', array('testOption1' => 'value1', 'testOption2' => 'value2'),
     *     array(array('Controller.action', 'OtherController.action'), array(...)),
     *     .
     *     .
     *     .
     * )
     * </code>
     *
     * Valid test options:
     * <ul>
     *   <li><b>UNIMPLEMENTED</b></li>
     * </ul>
     */
    public function getControllerActionsForTesting() {
        return array();
    }

    /**
     * Gets the string prefix used in the name of the expected/processed output files.
     */
    public function getOutputPrefix()
    {
        return str_replace('Test_Piwik_Integration_', '', get_class($this));
    }

    /**
     * Runs API tests.
     */
    protected function runApiTests($api, $params)
    {
        $testName = 'test_' . $this->getOutputPrefix();

        if ($api == 'all')
        {
            $this->setApiToCall(array());
            $this->setApiNotToCall(self::$defaultApiNotToCall);
        }
        else
        {
            if (!is_array($api))
            {
                $api = array($api);
            }

            $this->setApiToCall($api);
            $this->setApiNotToCall(array('API.getPiwikVersion'));
        }

        if (isset($params['disableArchiving']) && $params['disableArchiving'] === true)
        {
            Piwik_ArchiveProcessing::$forceDisableArchiving = true;
        }
        else
        {
            Piwik_ArchiveProcessing::$forceDisableArchiving = false;
        }

        if (isset($params['language']))
        {
            $this->changeLanguage($params['language']);
        }

        $testSuffix = isset($params['testSuffix']) ? $params['testSuffix'] : '';

        $this->_callGetApiCompareOutput(
            $testName . $testSuffix,
            isset($params['format']) ? $params['format'] : 'xml',
            isset($params['idSite']) ? $params['idSite'] : false,
            isset($params['date']) ? $params['date'] : false,
            isset($params['periods']) ? $params['periods'] : false,
            isset($params['setDateLastN']) ? $params['setDateLastN'] : false,
            isset($params['language']) ? $params['language'] : false,
            isset($params['segment']) ? $params['segment'] : false,
            isset($params['visitorId']) ? $params['visitorId'] : false,
            isset($params['abandonedCarts']) ? $params['abandonedCarts'] : false,
            isset($params['idGoal']) ? $params['idGoal'] : false,
            isset($params['apiModule']) ? $params['apiModule'] : false,
            isset($params['apiAction']) ? $params['apiAction'] : false,
            isset($params['otherRequestParameters']) ? $params['otherRequestParameters'] : array());

        // change the language back to en
        if ($this->lastLanguage != 'en')
        {
            $this->changeLanguage('en');
        }
    }

    /**
     * Runs controller tests.
     */
    protected function runControllerTests($actions, $params)
    {
        static $nonRequestParameters = array('testingLevelOverride' => null, 'userTypes' => null);

        $testName = 'test_' . $this->getOutputPrefix();

        // deal w/ any language changing hacks
        if (isset($params['language'])) {
            $this->changeLanguage($params['language']);
        }

        // separate request parameters from function parameters
        $requestParams = array();
        foreach ($params as $key => $value) {
            if (!isset($nonRequestParameters[$key])) {
                $requestParams[$key] = $value;
            }
        }

        $testSuffix = isset($params['testSuffix']) ? $params['testSuffix'] : '';

        $this->callWidgetsCompareOutput(
            $testName . $testSuffix,
            $actions,
            $requestParams,
            isset($params['userTypes']) ? $params['userTypes'] : false,
            isset($params['testingLevelOverride']) ? $params['testingLevelOverride'] : false);

        // change the language back to en
        if ($this->lastLanguage != 'en') {
            $this->changeLanguage('en');
        }
    }

    /**
     * changing the language within one request is a bit fancy
     * in order to keep the core clean, we need a little hack here
     *
     * @param string $langId
     */
    protected function changeLanguage( $langId )
    {
        if (isset($this->lastLanguage) && $this->lastLanguage != $langId)
        {
            $_GET['language'] = $langId;
            Piwik_Translate::reset();
            Piwik_Translate::getInstance()->reloadLanguage($langId);
        }

        $this->lastLanguage = $langId;
    }

    /**
     * Path where expected/processed output files are stored. Can be overridden.
     */
    public function getPathToTestDirectory()
    {
        /**
         * Use old path as long as files were not moved
         * @todo move files
         */
        //return dirname(__FILE__).DIRECTORY_SEPARATOR.'Integration';
        return dirname(dirname(__FILE__)).DIRECTORY_SEPARATOR.'integration';
    }

}