<?php

abstract class minifluxTestCase extends PHPUnit_Extensions_Selenium2TestCase
{
    protected $basePageHeading = NULL;
    protected $expectedPageUrl = NULL;
    protected $expectedDataSet = NULL;
    protected $expectedCounterPage = NULL;
    protected $expectedCounterUnread = '';

    protected $ignorePageTitle = FALSE;

    protected static $databaseConnection = NULL;
    protected static $databaseTester = NULL;

    private $waitTimeout = 5000;

    public static function browsers()
    {
        return json_decode(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_BROWSERS, true);
    }

    protected function setUp()
    {
        parent::setUp();

        // trigger database fixtures onSetUp routines
        $this->getDatabaseTester('fixture_feed1', TRUE)->onSetUp();

        // Set the base URL for the tests.
        $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_BASEURL);
    }

    public function doLoginIfRequired($url)
    {
        // (re)load the requested page
        $this->url($url);

        // check if login is need and login
        $elements = $this->elements($this->using('css selector')->value('body#login-page'));

        if (count($elements) === 1) {
            $this->url('?action=select-db&database='.DB_FILENAME);

            $this->byId('form-username')->click();
            $this->keys('admin');
            $this->byId('form-password')->click();
            $this->keys('admin');
            $this->byTag('form')->submit();

            $this->url($url);
        }
    }

    public static function tearDownAfterClass()
    {
        static::$databaseConnection = NULL;
        static::$databaseTester = NULL;
    }

    protected function assertPostConditions()
    {
        // counter exists on every page
        $this->assertEquals($this->expectedCounterPage, $this->getCounterPage(), 'page-counter differ from expectation');
        $this->assertEquals($this->expectedCounterUnread, $this->getCounterUnread(), 'unread counter differ from expectation');

        // url has not been changed (its likely that everything was done via javascript then)
        $this->assertEquals($this->expectedPageUrl, $this->url(), 'URL has been changed.');

        // some tests switch to a page where no counter exists and the expected
        // pagetitle doesn't match to definition.
        if ($this->ignorePageTitle === FALSE) {
            //remove LEFT-TO-RIGHT MARK char from string as the webdriver does it when using text() on the page <h[1|2|3]>
            $pagetitle = preg_replace('/\x{200E}/u', '', $this->title());
            $this->assertEquals($this->getExpectedPageTitle(), $pagetitle, 'page title differ from expectation');
        }

        // assert that the current database matches the expected database
        $expectedDataSetFiltered = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($this->expectedDataSet);
        $expectedDataSetFiltered->addIncludeTables(array('items'));
        $expectedDataSetFiltered->setExcludeColumnsForTable('items', array('updated'));

        // TODO: changes row order, why?
        //$actualDataSet = $this->getConnection()->createDataSet();
        $actualDataSet = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
        $actualDataSet->addTable('items', 'SELECT * FROM items');
        $actualDataSetFiltered = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($actualDataSet);
        $actualDataSetFiltered->setExcludeColumnsForTable('items', array('updated'));

        PHPUnit_Extensions_Database_TestCase::assertDataSetsEqual($expectedDataSetFiltered, $actualDataSetFiltered, 'Unexpected changes in database');
    }

    protected function getDataSet($dataSetFile, $appendFeed2 = TRUE)
    {
        $compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();

        $ds1 = new PHPUnit_Extensions_Database_DataSet_XmlDataSet(dirname(__FILE__).DIRECTORY_SEPARATOR.'datasets'.DIRECTORY_SEPARATOR.$dataSetFile.'.xml');
        $compositeDs->addDataSet($ds1);

        if ($appendFeed2) {
            // feed2 should be normaly untouched
            $ds2 = new PHPUnit_Extensions_Database_DataSet_XmlDataSet(dirname(__FILE__).DIRECTORY_SEPARATOR.'datasets'.DIRECTORY_SEPARATOR.'fixture_feed2.xml');
            $compositeDs->addDataSet($ds2);
        }

        return $compositeDs;
    }

    protected function getConnection()
    {
        if (is_null(static::$databaseConnection)) {
            // let Miniflux setup the database
            require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'common.php';

            if (!ENABLE_MULTIPLE_DB) {
                throw new Exception('Enable multiple databases support to run the tests!');
            }

            $picoDb = new PicoDb\Database(array(
                'driver' => 'sqlite',
                'filename' => \Model\Database\get_path(),
            ));

            $picoDb->schema()->check(Schema\VERSION);

            // make the database world writeable, maybe the current
            // user != webserver user
            chmod(\Model\Database\get_path(), 0666);

            // get pdo object
            $pdo = $picoDb->getConnection();

            // disable fsync! its awefull slow without transactions and I found
            // no way to use setDataSet function with transactions
            $pdo->exec("pragma synchronous = off;");

            static::$databaseConnection = new PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection($pdo, 'sqlite');
        }

        return static::$databaseConnection;
    }

    protected function getDatabaseTester($dataSetFile, $appendFeed2)
    {
        if (is_null(static::$databaseTester)) {
            $tester = new PHPUnit_Extensions_Database_DefaultTester($this->getConnection());

            // article/feed import on database->onSetUp();
            $tester->setSetUpOperation(PHPUnit_Extensions_Database_Operation_Factory::CLEAN_INSERT());
            $dataset = $this->getDataSet($dataSetFile, $appendFeed2);
            $rdataset = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($dataset);
            $rdataset->addSubStrReplacement('##TIMESTAMP##', substr((string)(time()-100), 0, -2));
            $tester->setDataSet($rdataset);

            static::$databaseTester = $tester;
        }

        return static::$databaseTester;
    }

    private function getCounterUnread()
    {
        $value = $this->element($this->using('id')->value('nav-counter'))->text();
        return $value;
    }

    private function getCounterPage()
    {
        $value = NULL;

        $elements = $this->elements($this->using('id')->value('page-counter'));

        if (count($elements) === 1) {
            $value = $elements[0]->text();
        }

        return $value;
    }

    private function waitForElementVisibility($element, $visible)
    {
        // return false in case of timeout
        try {
            $value = $this->waitUntil(function() use($element, $visible) {
                // a "No such Element" or "Stale Element Reference" exception is
                // valid if an object should disappear
                try {
                    $displaySize = $element->size();

                    if ((($visible === TRUE) && ($element->displayed() && $displaySize['height']>0 && $displaySize['width']>0))
                         || (($visible === FALSE) && ($element->displayed() === FALSE || $displaySize['height']=0 || $displaySize['width']=0))) {
                        return TRUE;
                    }
                }
                catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {

                    $noSuchElement = ($e->getCode() === PHPUnit_Extensions_Selenium2TestCase_WebDriverException::NoSuchElement
                                   || $e->getCode() === PHPUnit_Extensions_Selenium2TestCase_WebDriverException::StaleElementReference);

                    if (($visible === FALSE) && ($noSuchElement)) {
                        return TRUE;
                    } else {
                        throw $e;
                    }
                }
            }, $this->waitTimeout);
        }
        catch(PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {
            if ($e->getCode() === PHPUnit_Extensions_Selenium2TestCase_WebDriverException::Timeout) {
                return FALSE;
            } else {
                throw $e;
            }
        }

        return $value;
    }

    private function waitForElementCountByCssSelector($cssSelector, $elementCount)
    {
        // return false in case of timeout
        try {
            // Workaround for PHP < 5.4
            $CI = $this;

            $value = $this->waitUntil(function() use($cssSelector, $elementCount, $CI) {
                $elements = $CI->elements($CI->using('css selector')->value($cssSelector));

                if (count($elements) === $elementCount) {
                    return TRUE;
                }
            }, $this->waitTimeout);
        }
        catch(PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {
            if ($e->getCode() === PHPUnit_Extensions_Selenium2TestCase_WebDriverException::Timeout) {
                return FALSE;
            } else {
                throw $e;
            }
        }

        return $value;
    }

    private function waitForElementAttributeHasValue($element, $attribute, $attributeValue, $invertMatch = FALSE)
    {
        // return false in case of timeout
        try {
            $value = $this->waitUntil(function() use($element, $attribute, $attributeValue, $invertMatch) {
                $attributeHasValue = ($element->attribute($attribute) === $attributeValue);

                if (($attributeHasValue && !$invertMatch) || (!$attributeHasValue && $invertMatch)) {
                    return TRUE;
                }
            }, $this->waitTimeout);
        }
        catch(PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {
            if ($e->getCode() === PHPUnit_Extensions_Selenium2TestCase_WebDriverException::Timeout) {
                return FALSE;
            } else {
                throw $e;
            }
        }

        return $value;
    }

    private function waitForIconMarkRead($article, $visible)
    {
        $icon = $article->elements($article->using('css selector')->value('span.read-icon'));

        $value = $this->waitForElementVisibility($icon[0], $visible);
        return $value;
    }

    private function waitForIconBookmark($article, $visible)
    {
        $icon = $article->elements($article->using('css selector')->value('span.bookmark-icon'));

        $value = $this->waitForElementVisibility($icon[0], $visible);
        return $value;
    }

    public function getBasePageHeading()
    {
        /*
         * WORKAROUND: Its not possible to get an elements text content without
         * the text of its childs. Thats why we have to differ between
         * pageheadings with counter and without counter.
         */

        // text of its childs
        $pageHeading = $this->byCssSelector('div.page-header > h2:first-child')->text();

        // Some PageHeadings have a counter included
        $innerHeadingElements = $this->elements($this->using('css selector')->value('div.page-header > h2:first-child *'));

        if (count($innerHeadingElements) > 0)
        {
            $innerHeading = $innerHeadingElements[0]->text();
            $pageHeading = substr($pageHeading, 0, (strlen($innerHeading) * -1));
        }

        return $pageHeading;
    }

    public function getURLPageUnread()
    {
        return PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_BASEURL.'?action=unread';
    }

    public function getURLPageBookmarks()
    {
        return PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_BASEURL.'?action=bookmarks';
    }

    public function getURLPageHistory()
    {
        return PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_BASEURL.'?action=history';
    }

    public function getURLPageFirstFeed()
    {
        return PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_BASEURL.'?action=feed-items&feed_id=1';
    }

    public function getURLPagePreferences()
    {
        return PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_BASEURL.'?action=config';
    }

    public function getShortcutNextItemA()
    {
        return 'n';
    }

    public function getShortcutNextItemB()
    {
        return 'j';
    }

    public function getShortcutPreviousItemA()
    {
        return 'p';
    }

    public function getShortcutPreviousItemB()
    {
        return 'k';
    }

    public function getShortcutToogleReadStatus()
    {
        return 'm';
    }

    public function getShortcutToogleBookmarkStatus()
    {
        return 'f';
    }

    public function getShortcutGoToUnread()
    {
        return 'gu';
    }

    public function getArticles()
    {
        $cssSelector = 'article';

        $articles = $this->elements($this->using('css selector')->value($cssSelector));
        return $articles;
    }

    public function getArticlesUnread()
    {
        $cssSelector = 'article[data-item-status="unread"]';

        $articles = $this->elements($this->using('css selector')->value($cssSelector));
        return $articles;
    }

    public function getArticlesRead()
    {
        $cssSelector = 'article[data-item-status="read"]';

        $articles = $this->elements($this->using('css selector')->value($cssSelector));
        return $articles;
    }

    public function getArticlesNotBookmarked()
    {
        $cssSelector = 'article[data-item-bookmark="0"]';

        $articles = $this->elements($this->using('css selector')->value($cssSelector));
        return $articles;
    }

    public function getArticlesNotFromFeedOne()
    {
        $cssSelector = 'article:not(.feed-1)';

        $articles = $this->elements($this->using('css selector')->value($cssSelector));
        return $articles;
    }

    public function getArticleUnreadNotBookmarked()
    {
        $cssSelector = 'article[data-item-id="7c6afaa5"]';

        $article = $this->element($this->using('css selector')->value($cssSelector));
        return $article;
    }

    public function getArticleReadNotBookmarked()
    {
        $cssSelector = 'article[data-item-id="9b20eb66"]';

        $article = $this->element($this->using('css selector')->value($cssSelector));
        return $article;
    }

    public function getArticleUnreadBookmarked()
    {
        $cssSelector = 'article[data-item-id="7cb2809d"]';

        $article = $this->element($this->using('css selector')->value($cssSelector));
        return $article;
     }

    public function getArticleReadBookmarked()
    {
        $cssSelector = 'article[data-item-id="9fa78b54"]';

        $articles = $this->element($this->using('css selector')->value($cssSelector));
        return $articles;
    }

    public function getLinkReadStatusToogle($article)
    {
        $link = $article->element($article->using('css selector')->value('a.mark'));
        return $link;
    }

    public function getLinkBookmarkStatusToogle($article)
    {
        $link = $article->element($article->using('css selector')->value('a.bookmark'));
        return $link;
    }

    public function getLinkRemove($article)
    {
        $link = $article->element($article->using('css selector')->value('a.delete'));
        return $link;
    }

    public function getLinkFeedMarkReadHeader()
    {
        $link = $this->element($this->using('css selector')->value('div.page-header a[data-action="mark-feed-read"]'));
        return $link;
    }

    public function getLinkFeedMarkReadBottom()
    {
        $link = $this->element($this->using('css selector')->value('div#bottom-menu a[data-action="mark-feed-read"]'));
        return $link;
    }

    public function getLinkMarkAllReadHeader()
    {
        $link = $this->element($this->using('css selector')->value('div.page-header a[data-action="mark-all-read"]'));
        return $link;
    }

    public function getLinkMarkAllReadBottom()
    {
        $link = $this->element($this->using('css selector')->value('div#bottom-menu a[data-action="mark-all-read"]'));
        return $link;
    }

    public function getLinkFlushHistory()
    {
        $link = $this->element($this->using('css selector')->value('div.page-header a[href="?action=confirm-flush-history"]'));
        return $link;
    }

    public function getLinkDestructive()
    {
        $link = $this->element($this->using('css selector')->value('a.btn-red'));
        return $link;
    }

    public function waitForArticleIsCurrentArticle($article)
    {
        $isCurrent = $this->waitForElementAttributeHasValue($article, 'id', 'current-item');
        return $isCurrent;
    }

    public function waitForArticleIsNotCurrentArticle($article)
    {
        $isCurrent = $this->waitForElementAttributeHasValue($article, 'id', 'current-item', TRUE);
        return $isCurrent;
    }

    public function waitForIconMarkReadVisible($article)
    {
        $visible = $this->waitForIconMarkRead($article, TRUE);
        return $visible;
    }

    public function waitForIconMarkReadInvisible($article)
    {
        $invisible = $this->waitForIconMarkRead($article, FALSE);
        return $invisible;
    }

    public function waitForIconBookmarkVisible($article)
    {
        $visible = $this->waitForIconBookmark($article, TRUE);
        return $visible;
    }

    public function waitForIconBookmarkInvisible($article)
    {
        $invisible = $this->waitForIconBookmark($article, FALSE);
        return $invisible;
    }

    public function waitForArticleInvisible($article)
    {
        $invisible = $this->waitForElementVisibility($article, FALSE);
        return $invisible;
    }

    public function waitForArticlesMarkRead()
    {
        $cssSelector = 'article[data-item-status="unread"]';

        $read = $this->waitForElementCountByCssSelector($cssSelector, 0);
        return $read;
    }

    public function waitForAlert()
    {
        $cssSelector = 'p.alert';

        $visible = $this->waitForElementCountByCssSelector($cssSelector, 1);
        return $visible;
    }

    public function sendKeysAndWaitForPageLoaded($keys)
    {
        $this->keys($keys);

        // Workaround for PHP < 5.4
        $CI = $this;

        $this->waitUntil(function() use($CI) {
            $readyState = $CI->execute(array(
                'script' => 'return document.readyState;',
                'args'   => array()
            ));

            if ($readyState === 'complete') {
                return TRUE;
            }
        }, $this->waitTimeout);
    }

    public function setArticleAsCurrentArticle($article)
    {
        $script = 'document.getElementById("' .$article->attribute('id') .'").id = "current-item";'
                . 'return true';

        $this->execute(array(
            'script' => $script,
            'args'   => array()
        ));

        $result = $this->waitForArticleIsCurrentArticle($article);
        if ($result === FALSE) {
            throw new Exception('the article could not be set as current article.');
        }
    }
}
?>