<?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 AbstractRequestHandlerTest extends \PHPUnit_Extensions_Selenium2TestCase {

    const PARAM_HANDLER_NAME = 'ParamEchoHandler';
    const NEW_VIEW_HANDLER_NAME = 'NewViewHandler';
    const CLASSPATH_ON_SERVER = '/var/www/html/id1354-fw/classes/';
    const VIEWS_DIR_ON_SERVER = '/var/www/html/id1354-fw/views/';
    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 testParameters() {
        $this->sendRequestWithParameters(array());
        $this->sendRequestWithParameters(array('a' => ''));
        $this->sendRequestWithParameters(array('a' => '1'));
        $this->sendRequestWithParameters(array('a' => '1', 'b' => '2'));
        $this->sendRequestWithParameters(array('adfds' => '1sdfsdf', 'b' => ''));
        $this->sendRequestWithParameters(array('a' => '1', 'b' => '2', 'c' => '3', 'd' => '4', 'e' => '5'));
    }

    public function testMissingSetter() {
        $this->doTestMissingSetter(array());
        $this->doTestMissingSetter(array('a' => ''));
        $this->doTestMissingSetter(array('a' => '1'));
        $this->doTestMissingSetter(array('a' => '1', 'b' => '2'));
        $this->doTestMissingSetter(array('adfds' => '1sdfsdf', 'b' => ''));
    }

    public function testNextView() {
        $this->doTestNextView(array(), NULL, 'new-view');
        $this->doTestNextView(array('a' => 'a'), NULL, 'new-view');
        $this->doTestNextView(array('a' => ''), NULL, 'new-view');
        $this->doTestNextView(array('a' => 2), NULL, 'new-view');
        $this->doTestNextView(array('a' => NULL), NULL, 'new-view');
        $this->doTestNextView(array('a' => FALSE), NULL, 'new-view');
        $this->doTestNextView(array('a' => 'a', 'b' => 'b', 'c' => 'c'), 'dir1/dir2/', 'new-view');
        $this->doTestNextView(array('a' => 'a', 'b' => 'b', 'c' => 'c'), 'dir1/', 'new-view');
    }

    public function testNonExistingView() {
        try {
            $handler = $this->createNewViewHandler(array(), 'does-not-exist');
            $this->url(self::NEW_VIEW_HANDLER_NAME);
            $this->assertContains('No matching view was found for the path ', $this->source());
            $this->assertContains('/views/does-not-exist.php ', $this->source());
        } finally {
            \unlink($handler);
        }
    }

    public function testInvalidViewName() {
        $this->doTestInvalidViewName('');
        $this->doTestInvalidViewName(NULL);
        $this->doTestInvalidViewName(2);
        $this->doTestInvalidViewName(FALSE);
        $this->doTestInvalidViewName(TRUE);
    }

    public function testInvalidVarName() {
        $this->doTestInvalidVarName('');
        $this->doTestInvalidVarName(NULL);
        $this->doTestInvalidVarName(746);
        $this->doTestInvalidVarName(FALSE);
        $this->doTestInvalidVarName(TRUE);
    }

    private function doTestMissingSetter($params) {
        try {
            $handler = $this->createParamEchoHandler($params);
            $invalidParam = 'invalidParam';

            $queryParams = \array_merge($params, array($invalidParam => $invalidParam));
            $queryString = $this->createQueryString($queryParams);
            $this->url(self::PARAM_HANDLER_NAME . $queryString);
            $this->assertContains('Function ParamEchoHandler-&gt;set' . \ucfirst($invalidParam) .
                    ' is missing, but request has parameter ' . $invalidParam, $this->source());

            $queryParams = \array_merge(array($invalidParam => $invalidParam), $params);
            $queryString = $this->createQueryString($queryParams);
            $this->url(self::PARAM_HANDLER_NAME . $queryString);
            $this->assertContains('Function ParamEchoHandler-&gt;set' . \ucfirst($invalidParam) .
                    ' is missing, but request has parameter ' . $invalidParam, $this->source());
        } finally {
            \unlink($handler);
        }
    }

    private function doTestNextView($vars, $dirName, $fileName) {
        $viewPath = $this->createViewPath($dirName, $fileName);
        try {
            $handler = $this->createNewViewHandler($vars, $viewPath);
            $view = $this->createNewViewFile($vars, $dirName, $fileName);

            $this->url(self::NEW_VIEW_HANDLER_NAME);
            $this->assertContains($viewPath, $this->source());
            foreach ($vars as $name => $value) {
                $this->assertContains("$name: $value", $this->source());
            }
        } finally {
            \unlink($handler);
            \unlink($view);
            if (!empty($dirName)) {
                $dirToRemove = $dirName;
                while (($dirToRemove = $this->stripLastPathSegment($dirToRemove)) !== FALSE) {
                    \rmdir(self::VIEWS_DIR_ON_SERVER . DIRECTORY_SEPARATOR . $dirToRemove);
                }
            }
        }
    }

    private function doTestInvalidViewName($invalidName) {
        try {
            $handler = $this->createInvalidViewHandler($invalidName);
            $this->url(self::NEW_VIEW_HANDLER_NAME);
            $this->assertNotContains('No matching view was found for the path', $this->source());
        } finally {
            \unlink($handler);
        }
    }

    private function doTestInvalidVarName($varName) {
        try {
            $handler = $this->createInvalidVarHandler($varName);
            $this->url(self::NEW_VIEW_HANDLER_NAME);
            $this->assertContains('Id1354fw\Core\InternalServerErrorException: Invalid variable name:',
                                  $this->source());
            if (\is_integer($varName)) {
                $this->assertContains('Invalid variable name: ' . $varName, $this->source());
            }
            if ($varName === TRUE) {
                $this->assertContains('Invalid variable name: 1', $this->source());
            }
        } finally {
            \unlink($handler);
        }
    }

    private function stripLastPathSegment($path) {
        if (\strpos($path, DIRECTORY_SEPARATOR) === FALSE) {
            return FALSE;
        }
        $segments = \explode(DIRECTORY_SEPARATOR, $path);
        $strippedPath = '';
        for ($i = 0; $i < \count($segments) - 1; $i++) {
            $strippedPath = $strippedPath . $segments[$i] . DIRECTORY_SEPARATOR;
        }
        return \substr($strippedPath, 0, -1);
    }

    private function createViewPath($dirName, $fileName) {
        if (!empty($dirName)) {
            return $dirName . $fileName;
        } else {
            return $fileName;
        }
    }

    private function sendRequestWithParameters($params) {
        try {
            $handler = $this->createParamEchoHandler($params);
            $this->postParams($params);
            $this->getParams($params);
            $this->getAndPostParams($params);
            $this->invalidQueryString($params);
        } finally {
            \unlink($handler);
        }
    }

    private function sendGetAndPostParams($getParams, $postParams) {
        $queryString = $this->createQueryString($getParams);

        /*
         * Selenium can only drive the browser (by sending commands to the selenium server which 
         * controls the browser). This means we have to load a document with an html form, fill the form
         * and submit it in order to generate a post request. To simplify things, in this method we 
         * skip the browser (and selenium) and instead use curl to send a http post request directly
         * from the test code to the server under test.
         */
        $ch = \curl_init(self::SERVER_BASE_URL . self::PARAM_HANDLER_NAME . $queryString);
        \curl_setopt($ch, CURLOPT_POST, 1);
        \curl_setopt($ch, CURLOPT_POSTFIELDS, $postParams);
        \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $response = \curl_exec($ch);
        \curl_close($ch);

        $this->checkParams($postParams, $response);
        $this->checkParams($getParams, $response);
    }

    private function checkParams($params, $containingString) {
        if (\count($params) === 0) {
            return;
        }
        foreach ($params as $param => $value) {
            $this->assertContains("$param: " . $value, $containingString);
        }
    }

    private function getParams($params) {
        $queryString = $this->createQueryString($params);
        $this->url(self::PARAM_HANDLER_NAME . $queryString);
        $this->checkParams($params, $this->source());
    }

    private function invalidQueryString($params) {
        if (\count($params) === 0) {
            return '';
        }

        $queryString = $this->createQueryString($params);
        $invalidValue = '=invalidValue';

        $invalidQueryString = \str_replace('?', $invalidValue . '?', $queryString);
        $this->url(self::PARAM_HANDLER_NAME . $invalidQueryString);
        $this->assertContains('Request Handler Not Found.', $this->source());

        $this->url(self::PARAM_HANDLER_NAME . $queryString . '=invalidValue');
        $this->checkParams($params, $this->source());
        $this->assertContains(\array_pop($params) . $invalidValue, $this->source());
    }

    private function postParams($params) {
        $this->sendGetAndPostParams(NULL, $params);
    }

    private function getAndPostParams($params) {
        $numOfElems = \count($params);
        $this->sendGetAndPostParams(\array_slice($params, 0, $numOfElems / 2),
                                                 \array_slice($params, $numOfElems / 2,
                                                              $numOfElems - $numOfElems / 2));
    }

    private function createQueryString($params) {
        if (\count($params) === 0) {
            return '';
        }
        $queryString = '?';
        foreach ($params as $param => $value) {
            $queryString = $queryString . $param . '=' . $value . '&';
        }
        return \substr($queryString, 0, \strlen($queryString) - 1);
    }

    private function createParamEchoHandler($params) {
        $className = self::PARAM_HANDLER_NAME;
        $paramCode = $this->createParamHandling($params);
        $doExecuteCode = $this->createEchoHandlerDoExecute($params);
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
$paramCode
$doExecuteCode
}

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

    private function createParamHandling($params) {
        $paramCode = '';
        foreach ($params as $key => $value) {
            $paramCode = $paramCode . '    private $' . "$key;\n";
        }
        $paramCode = $paramCode . "\n";
        foreach ($params as $key => $value) {
            $keyPartOfProperty = \ucfirst($key);
            $paramCode = $paramCode . "    public function set$keyPartOfProperty(" . '$param' . ") {\n";
            $paramCode = $paramCode . '        $this->' . $key . ' = $param;' . "\n";
            $paramCode = $paramCode . "    }\n\n";
        }
        return $paramCode;
    }

    private function createEchoHandlerDoExecute($params) {
        $doExecCode = "    public function doExecute() {\n";
        foreach ($params as $param => $value) {
            $doExecCode = $doExecCode . "        echo '$param: ' " . '. $this->' . $param . ";\n";
        }
        $doExecCode = $doExecCode . "    }\n\n";
        return $doExecCode;
    }

    private function createNewViewHandler($vars, $viewFileName) {
        $className = self::NEW_VIEW_HANDLER_NAME;
        $doExecuteCode = $this->createNewViewHandlerDoExecute($vars, $viewFileName);
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
$doExecuteCode
}

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

    private function createInvalidVarHandler($varName) {
        if (\is_string($varName)) {
            $varName = '"' . $varName . '"';
        }
        if (\is_null($varName)) {
            $varName = 'NULL';
        }
        if (\is_bool($varName)) {
            if ($varName) {
                $varName = 'TRUE';
            } else {
                $varName = 'FALSE';
            }
        }
        $className = self::NEW_VIEW_HANDLER_NAME;
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
        \$this->addVariable($varName, 2);
    }
}

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

    private function createInvalidViewHandler($viewName) {
        if (\is_string($viewName)) {
            $viewName = '"' . $viewName . '"';
        }
        if (\is_null($viewName)) {
            $viewName = 'NULL';
        }
        if (\is_bool($viewName)) {
            if ($viewName) {
                $viewName = 'TRUE';
            } else {
                $viewName = 'FALSE';
            }
        }
        $className = self::NEW_VIEW_HANDLER_NAME;
        $handlerFile = self::CLASSPATH_ON_SERVER . $className . ".php";
        $handlerContent = <<<EOC
<?php
class $className extends \Id1354fw\View\AbstractRequestHandler {
    public function doExecute() {
        return $viewName;
    }
}

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

    private function createNewViewHandlerDoExecute($vars, $viewFileName) {
        $doExecCode = "    public function doExecute() {\n";
        foreach ($vars as $name => $value) {
            $doExecCode = $doExecCode . '        $this->addVariable(' . "'$name'" . ', ' . "'$value'" . ");\n";
        }
        if (\strlen($viewFileName) > 0) {
            $doExecCode = $doExecCode . "        return '$viewFileName';\n";
        }
        $doExecCode = $doExecCode . "    }\n\n";
        return $doExecCode;
    }

    private function createNewViewFile($vars, $dirName, $fileName) {
        $viewPath = $this->createViewPath($dirName, $fileName);
        $newViewEchoCode = $this->createNewViewEchoCode($vars);
        $viewFile = self::VIEWS_DIR_ON_SERVER . $viewPath . ".php";
        $viewContent = <<<EOC
<?php
echo '$viewPath';
$newViewEchoCode
EOC;

        if (!empty($dirName)) {
            \mkdir(self::VIEWS_DIR_ON_SERVER . $dirName, 0777, TRUE);
        }

        \file_put_contents($viewFile, $viewContent);
        return $viewFile;
    }

    private function createNewViewEchoCode($vars) {
        $newViewEchoCode = '';
        foreach ($vars as $name => $value) {
            $newViewEchoCode = $newViewEchoCode . "echo '$name: ' . " . '$' . $name . ";\n";
        }
        return $newViewEchoCode;
    }

}
