<?php

/*
 * Copyright (c) 2015, Leif Lindbäck
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are 
 * met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright 
 *    notice, this list of conditions and the following disclaimer in the 
 *    documentation and/or other materials provided with the distribution.
 * 
 * 3. Neither the name of the copyright holder nor the names of its 
 *    contributors may be used to endorse or promote products derived from 
 *    this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

namespace Id1354fw\View;

/**
 * @author Leif Lindback, leifl@kth.se
 * 
 * REMEMBER TO START SELENIUM SERVER WITH THE COMMAND selenium-server AT THE BASH SHELL.
 */
class SessionManagerTest extends \PHPUnit_Extensions_Selenium2TestCase {

    const FIRST_VISITED_SESSION_HANDLER_NAME = 'FirstVisitedSessionHandler';
    const SECOND_VISITED_SESSION_HANDLER_NAME = 'SecondVisitedSessionHandler';
    const THIRD_VISITED_SESSION_HANDLER_NAME = 'ThirdVisitedSessionHandler';
    const CLASSPATH_ON_SERVER = '/var/www/html/id1354-fw/classes/';
    const SERVER_BASE_URL = 'http://localhost/id1354-fw/';

    protected function setUp() {
        $this->setHost('localhost');
        $this->setPort(4444);
        $this->setBrowser('firefox');
        $this->setBrowserUrl(self::SERVER_BASE_URL);
    }

    public function testCurrentSession() {
        try {
            $firstHandler = $this->createFirstVisitedSessionHandler(TRUE, NULL);
            $secondHandler = $this->createSecondVisitedSessionHandler(TRUE, NULL, FALSE);
            $sessionId = $this->verifyThereIsASession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->assertEquals($sessionId,
                                $this->verifyThereIsASession(self::SECOND_VISITED_SESSION_HANDLER_NAME));
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testStopCurrentSession() {
        try {
            $firstHandler = $this->createFirstVisitedSessionHandler(TRUE, NULL);
            $secondHandler = $this->createSecondVisitedSessionHandler(FALSE, NULL, FALSE);
            $thirdHandler = $this->createThirdVisitedSessionHandler();
            $this->verifyThereIsASession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->verifyThereIsNoSession(self::SECOND_VISITED_SESSION_HANDLER_NAME);
            $this->verifyThereIsNoSession(self::THIRD_VISITED_SESSION_HANDLER_NAME);
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
            \unlink($thirdHandler);
        }
    }

    public function testNoCurrentSession() {
        try {
            $firstHandler = $this->createFirstVisitedSessionHandler(FALSE, NULL);
            $secondHandler = $this->createSecondVisitedSessionHandler(TRUE, NULL, FALSE);
            $this->verifyThereIsNoSession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->verifyThereIsNoSession(self::SECOND_VISITED_SESSION_HANDLER_NAME);
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testStopNoCurrentSession() {
        try {
            $firstHandler = $this->createFirstVisitedSessionHandler(FALSE, NULL);
            $secondHandler = $this->createSecondVisitedSessionHandler(FALSE, NULL, FALSE);
            $this->verifyThereIsNoSession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->verifyThereIsNoSession(self::SECOND_VISITED_SESSION_HANDLER_NAME);
            $this->assertContains('LogicException: SessionManager-&gt;stop() called whithout current session.',
                                  $this->source());
            $this->assertContains('Internal Server Error', $this->source());
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testRegenerateSessionId() {
        try {
            $firstHandler = $this->createFirstVisitedSessionHandler(TRUE, NULL);
            $secondHandler = $this->createSecondVisitedSessionHandler(TRUE, NULL, TRUE);
            $sessionId = $this->verifyThereIsASession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->assertNotEquals($sessionId,
                                   $this->verifyThereIsASession(self::SECOND_VISITED_SESSION_HANDLER_NAME));
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testSessionData() {
        try {
            $sessionData = array('a' => '1', 'b' => 2, 'c' => FALSE, 'd' => NULL);
            $firstHandler = $this->createFirstVisitedSessionHandler(TRUE, $sessionData);
            $secondHandler = $this->createSecondVisitedSessionHandler(TRUE, $sessionData, FALSE);
            $sessionId = $this->verifyThereIsASession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->assertEquals($sessionId,
                                $this->verifyThereIsASession(self::SECOND_VISITED_SESSION_HANDLER_NAME));
            foreach ($sessionData as $key => $value) {
                $this->assertContains("$key: $value", $this->source());
            }
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testSessionObjectData() {
        try {
            $objCreationCode = '$obj = new \stdClass(); $obj->field1 = "a"; $obj->field2 = 1234; $obj->field3 = TRUE;';

            $setHandlerClassName = self::FIRST_VISITED_SESSION_HANDLER_NAME;
            $setHandlerStartSessionCode = $this->createStartSessionCode(TRUE);
            $setHandlerFile = self::CLASSPATH_ON_SERVER . $setHandlerClassName . ".php";
            $setHandlerContent = <<<EOC
<?php
class $setHandlerClassName extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
        $setHandlerStartSessionCode
        $objCreationCode
        \$this->session->set('obj', \$obj);
    }
}
EOC;
            \file_put_contents($setHandlerFile, $setHandlerContent);

            $getHandlerClassName = self::SECOND_VISITED_SESSION_HANDLER_NAME;
            $getHandlerFile = self::CLASSPATH_ON_SERVER . $getHandlerClassName . ".php";
            $getHandlerContent = <<<EOC
<?php
class $getHandlerClassName extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
        \$obj = \$this->session->get('obj');
        foreach(\$obj as \$prop => \$val) {
            echo "\$prop: \$val";
        }
    }
}
EOC;
            \file_put_contents($getHandlerFile, $getHandlerContent);

            $this->url(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->url(self::SECOND_VISITED_SESSION_HANDLER_NAME);

            $this->assertContains('field1: a', $this->source());
            $this->assertContains('field2: 1234', $this->source());
            $this->assertContains('field3: 1', $this->source());
        } finally {
            \unlink($setHandlerFile);
            \unlink($getHandlerFile);
        }
    }

    public function testSessionDataAfterInvalidatingSession() {
        try {
            $sessionData = array('a' => '1', 'b' => 2, 'c' => TRUE);
            $firstHandler = $this->createFirstVisitedSessionHandler(TRUE, $sessionData);
            $secondHandler = $this->createSecondVisitedSessionHandler(FALSE, $sessionData, FALSE);
            $this->verifyThereIsASession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->verifyThereIsNoSession(self::SECOND_VISITED_SESSION_HANDLER_NAME);
            foreach ($sessionData as $key => $value) {
                $this->assertNotContains("$key: $value", $this->source());
            }
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testSessionDataAfterRegeneratingSessionId() {
        try {
            $sessionData = array('a' => '1', 'b' => 2, 'c' => TRUE);
            $firstHandler = $this->createFirstVisitedSessionHandler(TRUE, $sessionData);
            $secondHandler = $this->createSecondVisitedSessionHandler(TRUE, $sessionData, TRUE);
            $sessionId = $this->verifyThereIsASession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->assertNotEquals($sessionId,
                                   $this->verifyThereIsASession(self::SECOND_VISITED_SESSION_HANDLER_NAME));
            foreach ($sessionData as $key => $value) {
                $this->assertContains("$key: $value", $this->source());
            }
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testSetSessionDataNoSession() {
        $sessionData = array('a' => '1', 'b' => 2, 'c' => FALSE, 'd' => NULL);
        try {
            $firstHandler = $this->createFirstVisitedSessionHandler(FALSE, $sessionData);
            $this->verifyThereIsNoSession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->assertContains('LogicException: SessionManager-&gt;set(a, 1) called whithout current session.',
                                  $this->source());
            $this->assertContains('Internal Server Error', $this->source());
        } finally {
            \unlink($firstHandler);
        }
    }

    public function testGetSessionDataNoSession() {
        $varName = 'a';
        $sessionData = array($varName => '1');
        try {
            $firstHandler = $this->createFirstVisitedSessionHandler(TRUE, $sessionData);
            $secondHandler = $this->createSecondVisitedSessionHandler(FALSE, $sessionData, FALSE);
            $this->verifyThereIsASession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->verifyThereIsNoSession(self::SECOND_VISITED_SESSION_HANDLER_NAME);
            $this->assertContains(
                    "LogicException: SessionManager-&gt;get($varName) called whithout current session.",
                    $this->source());
        } finally {
            \unlink($firstHandler);
            \unlink($secondHandler);
        }
    }

    public function testSessionDataInvalidKey() {
        $this->doTestSessionDataInvalidKey('set', '');
        $this->doTestSessionDataInvalidKey('get', '');
        $this->doTestSessionDataInvalidKey('set', NULL);
        $this->doTestSessionDataInvalidKey('get', NULL);
        $this->doTestSessionDataInvalidKey('set', 928374);
        $this->doTestSessionDataInvalidKey('get', 908343);
        $this->doTestSessionDataInvalidKey('set', TRUE);
        $this->doTestSessionDataInvalidKey('get', TRUE);
        $this->doTestSessionDataInvalidKey('set', FALSE);
        $this->doTestSessionDataInvalidKey('get', FALSE);
    }

    public function doTestSessionDataInvalidKey($methodName, $key) {
        try {
            $handler = $this->createInvalidSessionDataKeyHandler($methodName, $key);
            $this->verifyThereIsNoSession(self::FIRST_VISITED_SESSION_HANDLER_NAME);
            $this->assertContains('LogicException: invalid key:', $this->source());
        } finally {
            \unlink($handler);
        }
    }

    private function verifyThereIsNoSession($url) {
        try {
            $this->url($url);
            $this->cookie()->get('PHPSESSID');
            $this->fail('Found session cookie when session should not exist');
        } catch (\PHPUnit_Extensions_Selenium2TestCase_Exception $ignore) {
            
        }
    }

    private function verifyThereIsASession($url) {
        try {
            $this->url($url);
            return $this->cookie()->get('PHPSESSID');
        } catch (\PHPUnit_Extensions_Selenium2TestCase_Exception $noSessionCookie) {
            $this->fail($noSessionCookie->getMessage());
        }
    }

    private function createFirstVisitedSessionHandler($startSession, $vars) {
        $className = self::FIRST_VISITED_SESSION_HANDLER_NAME;
        $startSessionCode = $this->createStartSessionCode($startSession);
        $setVarsCode = $this->createSetVarsCode($vars);
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
        $startSessionCode
        $setVarsCode
    }
}
EOC;
        \file_put_contents($handlerFile, $handlerContent);
        return $handlerFile;
    }

    private function createSecondVisitedSessionHandler($keepCurrentSession, $vars, $regenerateId) {
        $className = self::SECOND_VISITED_SESSION_HANDLER_NAME;
        $startSessionCode = $this->createStartSessionCode($regenerateId);
        $stopSessionCode = $this->createStopSessionCode($keepCurrentSession);
        $getVarsCode = $this->createGetVarsCode($vars);
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
        $startSessionCode
        $stopSessionCode
        $getVarsCode
    }
}
EOC;
        \file_put_contents($handlerFile, $handlerContent);
        return $handlerFile;
    }

    private function createThirdVisitedSessionHandler() {
        $className = self::THIRD_VISITED_SESSION_HANDLER_NAME;
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
    }
}
EOC;
        \file_put_contents($handlerFile, $handlerContent);
        return $handlerFile;
    }

    private function createStartSessionCode($startSession) {
        $startSessionCode = '';
        if ($startSession) {
            $startSessionCode = '        $this->session->restart();' . "\n";
        }
        return $startSessionCode;
    }

    private function createStopSessionCode($keepCurrentSession) {
        $keepCurrentSessionCode = '';
        if (!$keepCurrentSession) {
            $keepCurrentSessionCode = '        $this->session->invalidate();' . "\n";
        }
        return $keepCurrentSessionCode;
    }

    private function createSetVarsCode($vars) {
        if (!\is_array($vars)) {
            return '';
        }
        $setVarsCode = '';
        foreach ($vars as $key => $value) {
            $setVarsCode = $setVarsCode . '        $this->session->set(' . "'$key', '$value');\n";
        }
        return $setVarsCode;
    }

    private function createGetVarsCode($vars) {
        if (!\is_array($vars)) {
            return '';
        }
        $getVarsCode = '';
        foreach ($vars as $key => $value) {
            $getVarsCode = $getVarsCode . '        echo(' . "'$key: '" . ' . $this->session->get(' . "'$key'));\n";
        }
        return $getVarsCode;
    }

    private function createInvalidSessionDataKeyHandler($methodName, $key) {
        if (\is_string($key)) {
            $key = '"' . $key . '"';
        }
        if (\is_null($key)) {
            $key = 'NULL';
        }
        if (\is_bool($key)) {
            if ($key) {
                $key = 'TRUE';
            } else {
                $key = 'FALSE';
            }
        }
        $invalidMethodCallCode = $this->createInvalidMethodCallCode($methodName, $key);
        $className = self::FIRST_VISITED_SESSION_HANDLER_NAME;
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
        \$this->session->$invalidMethodCallCode
    }
}

EOC;
        \file_put_contents($handlerFile, $handlerContent);
        return $handlerFile;
    }

    private function createInvalidMethodCallCode($methodName, $key) {
        if ($methodName === 'set') {
            return "set($key, 2);";
        } elseif ($methodName === 'get') {
            return "get($key);";
        } else {
            throw new \LogicException("Invalid method name: $methodName");
        }
    }

}