<?php
/**
 * Slim - a micro PHP 5 framework
 *
 * @author      Josh Lockhart <info@joshlockhart.com>
 * @copyright   2011-2017 Josh Lockhart
 * @link        http://www.slimframework.com
 * @license     http://www.slimframework.com/license
 * @version     2.6.3
 *
 * MIT LICENSE
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

//Mock custom view
class CustomView extends \Slim\View
{
    public function render($template, $data = null) { echo "Custom view"; }
}

//Echo Logger
class EchoErrorLogger
{
   public function error($object) { echo get_class($object) .':'.$object->getMessage(); }
}

//Mock extending class
class Derived extends \Slim\Slim
{
	public static function getDefaultSettings()
	{
        return array_merge(
            array("late-static-binding" => true)
        , parent::getDefaultSettings());
	}
}

//Mock middleware
class CustomMiddleware extends \Slim\Middleware
{
    public function call()
    {
        $env = $this->app->environment();
        $res = $this->app->response();
        $env['slim.test'] = 'Hello';
        $this->next->call();
        $res->header('X-Slim-Test', 'Hello');
        $res->write('Hello');
    }
}

class SlimTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        //Remove environment mode if set
        unset($_ENV['SLIM_MODE']);

        //Reset session
        $_SESSION = array();

        //Prepare default environment variables
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'QUERY_STRING' => 'one=foo&two=bar',
            'SERVER_NAME' => 'slimframework.com',
        ));
    }

    /************************************************
     * INSTANTIATION
     ************************************************/

    /**
     * Test version constant is string
     */
    public function testHasVersionConstant()
    {
        $this->assertTrue(is_string(\Slim\Slim::VERSION));
    }

    /**
     * Test default instance properties
     */
    public function testDefaultInstanceProperties()
    {
        $s = new \Slim\Slim();
        $this->assertInstanceOf('\Slim\Http\Request', $s->request());
        $this->assertInstanceOf('\Slim\Http\Response', $s->response());
        $this->assertInstanceOf('\Slim\Router', $s->router());
        $this->assertInstanceOf('\Slim\View', $s->view());
        $this->assertInstanceOf('\Slim\Log', $s->getLog());
        $this->assertEquals(\Slim\Log::DEBUG, $s->getLog()->getLevel());
        $this->assertTrue($s->getLog()->getEnabled());
        $this->assertInstanceOf('\Slim\Environment', $s->environment());
    }

    /**
     * Test get default instance
     */
    public function testGetDefaultInstance()
    {
        $s = new \Slim\Slim();
        $s->setName('default'); //We must do this manually since a default app is already set in prev tests
        $this->assertEquals('default', $s->getName());
        $this->assertInstanceOf('\Slim\Slim', \Slim\Slim::getInstance());
        $this->assertSame($s, \Slim\Slim::getInstance());
    }

    /**
     * Test get named instance
     */
    public function testGetNamedInstance()
    {
        $s = new \Slim\Slim();
        $s->setName('foo');
        $this->assertSame($s, \Slim\Slim::getInstance('foo'));
    }

    /**
     * Test Slim autoloader ignores non-Slim classes
     *
     * Pre-conditions:
     * Instantiate a non-Slim class;
     *
     * Post-conditions:
     * Slim autoloader returns without requiring a class file;
     */
    public function testSlimAutoloaderIgnoresNonSlimClass()
    {
        $foo = new Foo();
    }

    /************************************************
     * SETTINGS
     ************************************************/

    /**
     * Test get setting that exists
     */
    public function testGetSettingThatExists()
    {
        $s = new \Slim\Slim();
        $this->assertEquals('./templates', $s->config('templates.path'));
    }

    /**
     * Test get setting that does not exist
     */
    public function testGetSettingThatDoesNotExist()
    {
        $s = new \Slim\Slim();
        $this->assertNull($s->config('foo'));
    }

    /**
     * Test set setting
     */
    public function testSetSetting()
    {
        $s = new \Slim\Slim();
        $this->assertEquals('./templates', $s->config('templates.path'));
        $s->config('templates.path', './tmpl');
        $this->assertEquals('./tmpl', $s->config('templates.path'));
    }

    /**
     * Test batch set settings
     */
    public function testBatchSetSettings()
    {
        $s = new \Slim\Slim();
        $this->assertEquals('./templates', $s->config('templates.path'));
        $this->assertTrue($s->config('debug'));
        $s->config(array(
            'templates.path' => './tmpl',
            'debug' => false
        ));
        $this->assertEquals('./tmpl', $s->config('templates.path'));
        $this->assertFalse($s->config('debug'));
    }

    /**
     * Test set settings recursively
     */
    public function testSetSettingsRecursively()
    {
        $config = array(
            'my_module' => array(
                'paths'  => array(
                    './my_module/path/1',
                ),
            )
        );

        $s = new \Slim\Slim($config);

        $override = array(
            'my_module' => array(
                'paths'  => array(
                    './my_module/path/2',
                    './my_module/path/3',
                ),
            )
        );

        // Test recursive batch behaviour
        $s->config($override, true);

        $expected =  array(
            'paths'  => array(
                './my_module/path/1',
                './my_module/path/2',
                './my_module/path/3',
            ),
        );

        $this->assertEquals($expected, $s->config('my_module'));

        // Test default batch behaviour
        $s = new \Slim\Slim($config);
        $s->config($override);

        $this->assertNotEquals($expected, $s->config('my_module'));
    }

    /************************************************
     * MODES
     ************************************************/

    /**
     * Test default mode
     */
    public function testGetDefaultMode()
    {
        $s = new \Slim\Slim();
        $this->assertEquals('development', $s->getMode());
    }

    /**
     * Test custom mode from environment
     */
    public function testGetModeFromEnvironment()
    {
        $_ENV['SLIM_MODE'] = 'production';
        $s = new \Slim\Slim();
        $this->assertEquals('production', $s->getMode());
    }

    /**
     * Test custom mode from app settings
     */
    public function testGetModeFromSettings()
    {
        $s = new \Slim\Slim(array(
            'mode' => 'test'
        ));
        $this->assertEquals('test', $s->getMode());
        }

    /**
     * Test mode configuration
     */
    public function testModeConfiguration()
    {
        $flag = 0;
        $configureTest = function () use (&$flag) {
            $flag = 'test';
        };
        $configureProduction = function () use (&$flag) {
            $flag = 'production';
        };
        $s = new \Slim\Slim(array('mode' => 'test'));
        $s->configureMode('test', $configureTest);
        $s->configureMode('production', $configureProduction);
        $this->assertEquals('test', $flag);
    }

    /**
     * Test mode configuration when mode does not match
     */
    public function testModeConfigurationWhenModeDoesNotMatch()
    {
        $flag = 0;
        $configureTest = function () use (&$flag) {
            $flag = 'test';
        };
        $s = new \Slim\Slim(array('mode' => 'production'));
        $s->configureMode('test', $configureTest);
        $this->assertEquals(0, $flag);
    }

    /**
     * Test mode configuration when not callable
     */
    public function testModeConfigurationWhenNotCallable()
    {
        $flag = 0;
        $s = new \Slim\Slim(array('mode' => 'production'));
        $s->configureMode('production', 'foo');
        $this->assertEquals(0, $flag);
    }

    /**
     * Test custom mode from getenv()
     */
    public function testGetModeFromGetEnv()
    {
        putenv('SLIM_MODE=production');
        $s = new \Slim\Slim();
        $this->assertEquals('production', $s->getMode());
    }

    /************************************************
     * ROUTING
     ************************************************/

    /**
     * Test GENERIC route
     */
    public function testGenericRoute()
    {
        $s = new \Slim\Slim();
        $callable = function () { echo "foo"; };
        $route = $s->map('/bar', $callable);
        $this->assertInstanceOf('\Slim\Route', $route);
        $this->assertEmpty($route->getHttpMethods());
    }

    /**
     * Test GET routes also get mapped as a HEAD route
     */
    public function testGetRouteIsAlsoMappedAsHead()
    {
        $s = new \Slim\Slim();
        $route = $s->get('/foo', function () {});
        $this->assertTrue($route->supportsHttpMethod(\Slim\Http\Request::METHOD_GET));
        $this->assertTrue($route->supportsHttpMethod(\Slim\Http\Request::METHOD_HEAD));
    }

    /**
     * Test GET route
     */
    public function testGetRoute()
    {
        $s = new \Slim\Slim();
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $route = $s->get('/bar', $mw1, $mw2, $callable);
        $s->call();
        $this->assertEquals('foobarxyz', $s->response()->body());
        $this->assertEquals('/bar', $route->getPattern());
        $this->assertSame($callable, $route->getCallable());
    }

    /**
     * Test POST route
     */
    public function testPostRoute()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'POST',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $route = $s->post('/bar', $mw1, $mw2, $callable);
        $s->call();
        $this->assertEquals('foobarxyz', $s->response()->body());
        $this->assertEquals('/bar', $route->getPattern());
        $this->assertSame($callable, $route->getCallable());
    }

    /**
     * Test PUT route
     */
    public function testPutRoute()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'PUT',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $route = $s->put('/bar', $mw1, $mw2, $callable);
        $s->call();
        $this->assertEquals('foobarxyz', $s->response()->body());
        $this->assertEquals('/bar', $route->getPattern());
        $this->assertSame($callable, $route->getCallable());
    }

    /**
     * Test PATCH route
     */
    public function testPatchRoute()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'PATCH',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $route = $s->patch('/bar', $mw1, $mw2, $callable);
        $s->call();
        $this->assertEquals('foobarxyz', $s->response()->body());
        $this->assertEquals('/bar', $route->getPattern());
        $this->assertSame($callable, $route->getCallable());
    }

    /**
     * Test DELETE route
     */
    public function testDeleteRoute()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'DELETE',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $route = $s->delete('/bar', $mw1, $mw2, $callable);
        $s->call();
        $this->assertEquals('foobarxyz', $s->response()->body());
        $this->assertEquals('/bar', $route->getPattern());
        $this->assertSame($callable, $route->getCallable());
    }

    /**
     * Test OPTIONS route
     */
    public function testOptionsRoute()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'OPTIONS',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $route = $s->options('/bar', $mw1, $mw2, $callable);
        $s->call();
        $this->assertEquals('foobarxyz', $s->response()->body());
        $this->assertEquals('/bar', $route->getPattern());
        $this->assertSame($callable, $route->getCallable());
    }

    /**
    * Test route groups
    */
    public function testRouteGroups()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'GET',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar/baz', //<-- Virtual'
        ));
        $s = new \Slim\Slim();
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $s->group('/bar', $mw1, function () use ($s, $mw2, $callable) {
            $s->get('/baz', $mw2, $callable);
        });
        $s->call();
        $this->assertEquals('foobarxyz', $s->response()->body());
    }

    /*
     * Test ANY route
     */
    public function testAnyRoute()
    {
        $mw1 = function () { echo "foo"; };
        $mw2 = function () { echo "bar"; };
        $callable = function () { echo "xyz"; };
        $methods = array('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS');
        foreach ($methods as $i => $method) {
            \Slim\Environment::mock(array(
                'REQUEST_METHOD' => $method,
                'SCRIPT_NAME' => '/foo', //<-- Physical
                'PATH_INFO' => '/bar', //<-- Virtual
            ));
            $s = new \Slim\Slim();
            $route = $s->any('/bar', $mw1, $mw2, $callable);
            $s->call();
            $this->assertEquals('foobarxyz', $s->response()->body());
            $this->assertEquals('/bar', $route->getPattern());
            $this->assertSame($callable, $route->getCallable());
        }
    }

    /**
     * Test if route does NOT expect trailing slash and URL has one
     */
    public function testRouteWithoutSlashAndUrlWithOne()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar/', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () { echo "xyz"; });
        $s->call();
        $this->assertEquals(404, $s->response()->status());
    }

    /**
     * Tests if route will match in case-insensitive manner if configured to do so
     */
    public function testRouteMatchesInCaseInsensitiveMannerIfConfigured()
    {
        \Slim\Environment::mock(array(
            'PATH_INFO' => '/BaR', // Does not match route case
        ));
        $s = new \Slim\Slim(array('routes.case_sensitive' => false));
        $route = $s->get('/bar', function () { echo "xyz"; });
        $s->call();
        $this->assertEquals(200, $s->response()->status());
        $this->assertEquals('xyz', $s->response()->body());
        $this->assertEquals('/bar', $route->getPattern());
    }

    /**
     * Test if route contains URL encoded characters
     */
    public function testRouteWithUrlEncodedCharacters()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar/jo%20hn/smi%20th', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/bar/:one/:two', function ($one, $two) { echo $one . $two; });
        $s->call();
        $this->assertEquals('jo hnsmi th', $s->response()->body());
    }

    /************************************************
     * VIEW
     ************************************************/

    /**
     * Test set view with string class name
     */
    public function testSetSlimViewFromString()
    {
        $s = new \Slim\Slim();
        $this->assertInstanceOf('\Slim\View', $s->view());
        $s->view('CustomView');
        $this->assertInstanceOf('CustomView', $s->view());
    }

    /**
     * Test set view with object instance
     */
    public function testSetSlimViewFromInstance()
    {
        $s = new \Slim\Slim();
        $this->assertInstanceOf('\Slim\View', $s->view());
        $s->view(new CustomView());
        $this->assertInstanceOf('CustomView', $s->view());
    }

    /**
     * Test view data is transferred to newer view
     */
    public function testViewDataTransfer()
    {
        $data = array('foo' => 'bar');
        $s = new \Slim\Slim();
        $s->view()->setData($data);
        $s->view('CustomView');
        $this->assertSame($data, $s->view()->getData());
    }

    /************************************************
     * RENDERING
     ************************************************/

    /**
     * Test template path is passed to view
     */
    public function testViewGetsTemplatesPath()
    {
        $path = dirname(__FILE__) . '/templates';
        $s = new \Slim\Slim(array('templates.path' => $path));
        $this->assertEquals($s->view->getTemplatesDirectory(), $path);
    }

    /**
     * Test render with template and data
     */
    public function testRenderTemplateWithData()
    {
        $s = new \Slim\Slim(array('templates.path' => dirname(__FILE__) . '/templates'));
        $s->get('/bar', function () use ($s) {
            $s->render('test.php', array('foo' => 'bar'));
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals(200, $status);
        $this->assertEquals('test output bar', $body);
    }

    /**
     * Test render with template and data and status
     */
    public function testRenderTemplateWithDataAndStatus()
    {
        $s = new \Slim\Slim(array('templates.path' => dirname(__FILE__) . '/templates'));
        $s->get('/bar', function () use ($s) {
            $s->render('test.php', array('foo' => 'bar'), 500);
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals(500, $status);
        $this->assertEquals('test output bar', $body);
    }

    /************************************************
     * LOG
     ************************************************/

    /**
     * Test get log
     *
     * This asserts that a Slim app has a default Log
     * upon instantiation. The Log itself is tested
     * separately in another file.
     */
    public function testGetLog()
    {
        $s = new \Slim\Slim();
        $this->assertInstanceOf('\Slim\Log', $s->getLog());
    }

    /************************************************
     * HTTP CACHING
     ************************************************/

    /**
     * Test Last-Modified match
     */
    public function testLastModifiedMatch()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'GET',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'HTTP_IF_MODIFIED_SINCE' => 'Sun, 03 Oct 2010 21:00:52 GMT',
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->lastModified(1286139652);
        });
        $s->call();
        $this->assertEquals(304, $s->response()->status());
    }

    /**
     * Test Last-Modified match
     */
    public function testLastModifiedDoesNotMatch()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'IF_MODIFIED_SINCE' => 'Sun, 03 Oct 2010 21:00:52 GMT',
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->lastModified(1286139250);
        });
        $s->call();
        $this->assertEquals(200, $s->response()->status());
    }

    public function testLastModifiedOnlyAcceptsIntegers()
    {
        $this->setExpectedException('\InvalidArgumentException');
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->lastModified('Test');
        });
        $s->call();
    }

    /**
     * Test Last Modified header format
     */
    public function testLastModifiedHeaderFormat()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->lastModified(1286139652);
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertTrue(isset($header['Last-Modified']));
        $this->assertEquals('Sun, 03 Oct 2010 21:00:52 GMT', $header['Last-Modified']);
    }

    /**
     * Test ETag matches
     */
    public function testEtagMatches()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'HTTP_IF_NONE_MATCH' => '"abc123"',
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->etag('abc123');
        });
        $s->call();
        $this->assertEquals(304, $s->response()->status());
    }

    /**
     * Test ETag does not match
     */
    public function testEtagDoesNotMatch()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'IF_NONE_MATCH' => '"abc1234"',
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->etag('abc123');
        });
        $s->call();
        $this->assertEquals(200, $s->response()->status());
    }

    /**
     * Test ETag with invalid type
     */
    public function testETagWithInvalidType()
    {
        $this->setExpectedException('\InvalidArgumentException');
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'IF_NONE_MATCH' => '"abc1234"',
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->etag('123','foo');
        });
        $s->call();
    }

    /**
     * Test Expires
     */
    public function testExpiresAsString()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->expires('5 days');
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertTrue(isset($header['Expires']));

        $this->assertEquals(
          strtotime('5 days'),
          strtotime($header['Expires']),
          1 // delta
        );
    }

    /**
     * Test Expires
     */
    public function testExpiresAsInteger()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $fiveDaysFromNow = time() + (60 * 60 * 24 * 5);
        $expectedDate = gmdate('D, d M Y H:i:s T', $fiveDaysFromNow);
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s, $fiveDaysFromNow) {
            $s->expires($fiveDaysFromNow);
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertTrue(isset($header['Expires']));
        $this->assertEquals($header['Expires'], $expectedDate);
    }

    /************************************************
     * COOKIES
     ************************************************/

    /**
     * Set cookie
     *
     * This tests that the Slim application instance sets
     * a cookie in the HTTP response header. This does NOT
     * test the implementation of setting the cookie; that is
     * tested in a separate file.
     */
    public function testSetCookie()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->setCookie('foo', 'bar', '2 days');
            $s->setCookie('foo1', 'bar1', '2 days');
        });
        $s->call();
        $cookie1 = $s->response->cookies->get('foo');
        $cookie2 = $s->response->cookies->get('foo1');
        $this->assertEquals(2, count($s->response->cookies));
        $this->assertEquals('bar', $cookie1['value']);
        $this->assertEquals('bar1', $cookie2['value']);
    }

    /**
     * Test get cookie
     *
     * This method ensures that the `Cookie:` HTTP request
     * header is parsed if present, and made accessible via the
     * Request object.
     */
    public function testGetCookie()
    {
        \Slim\Environment::mock(array(
            'REQUEST_METHOD' => 'GET',
            'REMOTE_ADDR' => '127.0.0.1',
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'QUERY_STRING' => 'one=foo&two=bar',
            'SERVER_NAME' => 'slimframework.com',
            'SERVER_PORT' => 80,
            'HTTP_COOKIE' => 'foo=bar; foo2=bar2',
            'slim.url_scheme' => 'http',
            'slim.input' => '',
            'slim.errors' => @fopen('php://stderr', 'w')
        ));
        $s = new \Slim\Slim();
        $this->assertEquals('bar', $s->getCookie('foo'));
        $this->assertEquals('bar2', $s->getCookie('foo2'));
    }

    /**
     * Test get cookie when cookie does not exist
     */
    public function testGetCookieThatDoesNotExist()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $this->assertNull($s->getCookie('foo'));
    }

    /**
     * Test delete cookie
     *
     * This method ensures that the `Set-Cookie:` HTTP response
     * header is set. The implementation of setting the response
     * cookie is tested separately in another file.
     */
    public function testDeleteCookie()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/bar', //<-- Virtual
            'COOKIE' => 'foo=bar; foo2=bar2',
        ));
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->setCookie('foo', 'bar');
            $s->deleteCookie('foo');
        });
        $s->call();
        $cookie = $s->response->cookies->get('foo');
        $this->assertEquals(1, count($s->response->cookies));
        $this->assertEquals('', $cookie['value']);
        $this->assertLessThan(time(), $cookie['expires']);
    }

    /************************************************
     * HELPERS
     ************************************************/

    /**
     * Test get filesystem path to Slim app root directory
     */
    public function testGetRoot()
    {
        $_SERVER['DOCUMENT_ROOT'] = dirname(__FILE__); //<-- No trailing slash
        $s = new \Slim\Slim();
        $this->assertEquals($_SERVER['DOCUMENT_ROOT'] . '/foo/', $s->root()); //<-- Appends physical app path with trailing slash
    }

    /**
     * Test stop
     */
    public function testStop()
    {
        $this->setExpectedException('\Slim\Exception\Stop');
        $s = new \Slim\Slim();
        $s->stop();
    }

    /**
     * Test stop with subsequent output
     */
    public function testStopWithSubsequentOutput()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            echo "Foo"; //<-- Should be in response body!
            $s->stop();
            echo "Bar"; //<-- Should not be in response body!
        });
        $s->call();
        $this->assertEquals('Foo', $s->response()->body());
    }

    /**
     * Test stop with output buffer on and pre content
     */
    public function testStopOutputWithOutputBufferingOnAndPreContent()
    {
        $this->expectOutputString('1.2.Foo.3'); //<-- PHP unit uses OB here
        echo "1.";
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            echo "Foo";
            $s->stop();
        });
        echo "2.";
        $s->run();      //<-- Needs to be run to actually echo body
        echo ".3";
    }

    /**
     * Test stop does not leave output buffers open
     */
    public function testStopDoesNotLeaveOutputBuffersOpen()
    {
        $level_start = ob_get_level();
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->stop();
        });
        $s->run();
        $this->assertEquals($level_start, ob_get_level());
    }

    /**
     * Test halt
     */
    public function testHalt()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            echo "Foo!"; //<-- Should not be in response body!
            $s->halt(500, 'Something broke');
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals(500, $status);
        $this->assertEquals('Something broke', $body);
    }

    /**
     * Test halt with output buffering and pre content
     */
    public function testHaltOutputWithOutputBufferingOnAndPreContent()
    {
        $this->expectOutputString('1.2.Something broke.3'); //<-- PHP unit uses OB here
        echo "1.";
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            echo "Foo!"; //<-- Should not be in response body!
            $s->halt(500, 'Something broke');
        });
        echo "2.";
        $s->run();
        echo ".3";
    }

    /**
     * Test halt does not leave output buffers open
     */
    public function testHaltDoesNotLeaveOutputBuffersOpen()
    {
        $level_start = ob_get_level();
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->halt(500, '');
        });
        $s->run();
        $this->assertEquals($level_start, ob_get_level());
    }

    /**
     * Test pass cleans buffer and throws exception
     */
    public function testPass()
    {
        ob_start();
        $s = new \Slim\Slim();
        echo "Foo";
        try {
            $s->pass();
            $this->fail('Did not catch Slim_Exception_Pass');
        } catch ( \Slim\Exception\Pass $e ) {}
        $output = ob_get_clean();
        $this->assertEquals('', $output);
    }

    /**
     * Test pass when there is a subsequent fallback route
     */
    public function testPassWithSubsequentRoute()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/name/Frank', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/name/Frank', function () use ($s) {
            echo "Fail"; //<-- Should not be in response body!
            $s->pass();
        });
        $s->get('/name/:name', function ($name) {
            echo $name; //<-- Should be in response body!
        });
        $s->call();
        $this->assertEquals('Frank', $s->response()->body());
    }

    /**
     * Test pass when there is not a subsequent fallback route
     */
    public function testPassWithoutSubsequentRoute()
    {
        \Slim\Environment::mock(array(
            'SCRIPT_NAME' => '/foo', //<-- Physical
            'PATH_INFO' => '/name/Frank', //<-- Virtual
        ));
        $s = new \Slim\Slim();
        $s->get('/name/Frank', function () use ($s) {
            echo "Fail"; //<-- Should not be in response body!
            $s->pass();
        });
        $s->call();
        $this->assertEquals(404, $s->response()->status());
    }

    /**
     * Test content type
     */
    public function testContentType()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->contentType('application/json');
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals('application/json', $header['Content-Type']);
    }

    /**
     * Test status
     */
    public function testStatus()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->status(403);
        });
        $s->call();
        $this->assertEquals(403, $s->response()->status());
    }

    /**
     * Test URL for
     */
    public function testSlimUrlFor()
    {
        $s = new \Slim\Slim();
        $s->get('/hello/:name', function () {})->name('hello');
        $this->assertEquals('/foo/hello/Josh', $s->urlFor('hello', array('name' => 'Josh'))); //<-- Prepends physical path!
    }

    /**
     * Test redirect sets status and header
     */
    public function testRedirect()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            echo "Foo"; //<-- Should not be in response body!
            $s->redirect('/somewhere/else', 303);
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals(303, $status);
        $this->assertEquals('/somewhere/else', $header['Location']);
        $this->assertEquals('', $body);
    }

    /************************************************
     * RUNNER
     ************************************************/

    /**
     * Test that runner sends headers and body
     */
    public function testRun()
    {
        $this->expectOutputString('Foo');
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            echo "Foo";
        });
        $s->run();
    }

    /**
     * Test runner output with output buffering on and pre content
     */
    public function testRunOutputWithOutputBufferingOnAndPreContent()
    {
      $this->expectOutputString('1.2.Foo.3');  //<-- PHP unit uses OB here
      $s = new \Slim\Slim();
      echo "1.";
      $s->get('/bar', function () use ($s) {
          echo "Foo";
      });
      echo "2.";
      $s->run();
      echo ".3";
    }

    /**
     * Test that runner does not leave output buffers open
     */
    public function testRunDoesNotLeaveAnyOutputBuffersOpen()
    {
      $level_start = ob_get_level();
      $s = new \Slim\Slim();
      $s->get('/bar', function () use ($s) {});
      $s->run();
      $this->assertEquals($level_start, ob_get_level());
    }

    /************************************************
     * MIDDLEWARE
     ************************************************/

    /**
     * Test add middleware
     *
     * This asserts that middleware are queued and called
     * in sequence. This also asserts that the environment
     * variables are passed by reference.
     */
    public function testAddMiddleware()
    {
        $this->expectOutputString('FooHello');
        $s = new \Slim\Slim();
        $s->add(new CustomMiddleware()); //<-- See top of this file for class definition
        $s->get('/bar', function () {
            echo 'Foo';
        });
        $s->run();
        $this->assertEquals('Hello', $s->response()->header('X-Slim-Test'));
    }

    /**
     * Test exception when adding circular middleware queues
     *
     * This asserts that the same middleware can NOT be queued twice (usually by accident).
     * Circular middleware stack causes a troublesome to debug PHP Fatal error:
     *
     * > Fatal error: Maximum function nesting level of '100' reached. aborting!
     */
    public function testFailureWhenAddingCircularMiddleware()
    {
        $this->setExpectedException('\RuntimeException');
        $middleware = new CustomMiddleware;
        $s = new \Slim\Slim;
        $s->add($middleware);
        $s->add(new CustomMiddleware);
        $s->add($middleware);
        $s->run();
    }

    /************************************************
     * FLASH MESSAGING
     ************************************************/

    public function testSetFlashForNextRequest()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->flash('info', 'bar');
        });
        $this->assertFalse(isset($_SESSION['slim.flash']));
        $s->run();
        $this->assertEquals('bar', $_SESSION['slim.flash']['info']);
    }

    public function testSetFlashForCurrentRequest()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->flashNow('info', 'bar');
        });
        $s->run();
        $env = $s->environment();
        $this->assertEquals('bar', $env['slim.flash']['info']);
    }

    public function testKeepFlashForNextRequest()
    {
        $_SESSION['slim.flash'] = array('info' => 'Foo');
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->flashKeep();
        });
        $s->run();
        $this->assertEquals('Foo', $_SESSION['slim.flash']['info']);
    }

    public function testFlashData()
    {
        $s = new \Slim\Slim();
        $s->get('/bar', function () use ($s) {
            $s->flashNow('info', 'bar');
        });
        $s->run();
        $this->assertEquals(array('info' => 'bar'), $s->flashData());
    }

    /************************************************
     * NOT FOUND HANDLING
     ************************************************/

    /**
     * Test custom Not Found handler
     */
    public function testNotFound()
    {
        $s = new \Slim\Slim();
        $s->notFound(function () {
            echo "Not Found";
        });
        $s->get('/foo', function () {});
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals(404, $status);
        $this->assertEquals('Not Found', $body);
    }

    /************************************************
     * ERROR HANDLING
     ************************************************/

    /**
     * Test default and custom error handlers
     *
     * Pre-conditions:
     * Invoked app route calls default error handler;
     *
     * Post-conditions:
     * Response status code is 500;
     */
    public function testSlimError()
    {
        $s = new \Slim\Slim(array(
            "log.enabled" => false
        ));
        $s->get('/bar', function () use ($s) {
            $s->error();
        });
        $s->call();
        $this->assertEquals(500, $s->response()->status());
    }

    /**
     * Test default error handler logs the error when debug is false.
     *
     * Pre-conditions:
     * Invoked app route calls default error handler;
     *
     * Post-conditions:
     * Error log is called
     */
    public function testDefaultHandlerLogsTheErrorWhenDebugIsFalse()
    {
        $s = new \Slim\Slim(array('debug' => false));
        $s->container->singleton('log', function ($c) {
            return new EchoErrorLogger();
        });
        $s->get('/bar', function () use ($s) {
            throw new \InvalidArgumentException('my specific error message');
        });

        ob_start();
        $s->run();
        $output = ob_get_clean();
        $this->assertTrue(strpos($output, 'InvalidArgumentException:my specific error message') !== false);
    }

    /**
     * Test triggered errors are converted to ErrorExceptions
     *
     * Pre-conditions:
     * Custom error handler defined;
     * Invoked app route triggers error;
     *
     * Post-conditions:
     * Response status is 500;
     * Response body is equal to triggered error message;
     * Error handler's argument is ErrorException instance;
     */
    public function DISABLEDtestTriggeredErrorsAreConvertedToErrorExceptions()
    {
        $s = new \Slim\Slim(array(
            'debug' => false
        ));
        $s->error(function ( $e ) {
            if ($e instanceof \ErrorException) {
                echo $e->getMessage();
            }
        });
        $s->get('/bar', function () {
            trigger_error('Foo I say!');
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals(500, $status);
        $this->assertEquals('Foo I say!', $body);
    }

    /**
     * Test error triggered with multiple applications
     *
     * Pre-conditions:
     * Multiple Slim apps are instantiated;
     * Both apps are run;
     * One app returns 200 OK;
     * One app triggers an error;
     *
     * Post-conditions:
     * One app returns 200 OK with no Exceptions;
     * One app returns 500 Error;
     * Error triggered does not affect other app;
     */
    public function testErrorWithMultipleApps()
    {
        $s1 = new \Slim\Slim(array(
            'debug' => false,
            'log.enabled' => false
        ));
        $s2 = new \Slim\Slim();
        $s1->get('/bar', function () use ($s1) {
            $s1->error();
        });
        $s2->get('/bar', function () {
            echo 'success';
        });
        $s1->call();
        $s2->call();
        $this->assertEquals(500, $s1->response()->status());
        $this->assertEquals(200, $s2->response()->status());
    }

    /**
     * Test custom error handler uses existing Response object
     */
    public function testErrorHandlerUsesCurrentResponseObject()
    {
        $s = new \Slim\Slim(array(
            'debug' => false
        ));
        $s->error(function ( \Exception $e ) use ($s) {
            $r = $s->response();
            $r->status(503);
            $r->write('Foo');
            $r['X-Powered-By'] = 'Slim';
            echo 'Bar';
        });
        $s->get('/bar', function () {
            throw new \Exception('Foo');
        });
        $s->call();
        list($status, $header, $body) = $s->response()->finalize();
        $this->assertEquals(503, $status);
        $this->assertEquals('FooBar', $body);
        $this->assertEquals('Slim', $header['X-Powered-By']);
    }

    /**
     * Test custom global error handler
     */
    public function testHandleErrors()
    {
        $defaultErrorReporting = error_reporting();

        // Test 1
        error_reporting(E_ALL ^ E_NOTICE); // <-- Report all errors EXCEPT notices
        try {
            \Slim\Slim::handleErrors(E_NOTICE, 'test error', 'Slim.php', 119);
        } catch (\ErrorException $e) {
            $this->fail('Slim::handleErrors reported a disabled error level.');
        }

        // Test 2
        error_reporting(E_ALL | E_STRICT); // <-- Report all errors, including E_STRICT
        try {
            \Slim\Slim::handleErrors(E_STRICT, 'test error', 'Slim.php', 119);
            $this->fail('Slim::handleErrors didn\'t report a enabled error level');
        } catch (\ErrorException $e) {}

        error_reporting($defaultErrorReporting);
    }

    /**
     * Slim should keep reference to a callable error callback
     */
    public function testErrorHandler() {
        $s = new \Slim\Slim();
        $errCallback = function () { echo "404"; };
        $s->error($errCallback);
        $this->assertSame($errCallback, PHPUnit_Framework_Assert::readAttribute($s, 'error'));
    }

    /**
     * Slim should throw a Slim_Exception_Stop if error callback is not callable
     */
    public function testErrorHandlerIfNotCallable() {
        $this->setExpectedException('\Slim\Exception\Stop');
        $s = new \Slim\Slim(array("log.enabled" => false));
        $errCallback = 'foo';
        $s->error($errCallback);
    }

    /**
     * Slim should keep reference to a callable NotFound callback
     */
    public function testNotFoundHandler() {
        $s = new \Slim\Slim();
        $notFoundCallback = function () { echo "404"; };
        $s->notFound($notFoundCallback);
        $this->assertSame($notFoundCallback, PHPUnit_Framework_Assert::readAttribute($s, 'notFound'));
    }

    /**
     * Slim should throw a Slim_Exception_Stop if NotFound callback is not callable
     */
    public function testNotFoundHandlerIfNotCallable() {
        $this->setExpectedException('\Slim\Exception\Stop');
        $s = new \Slim\Slim();
        $notFoundCallback = 'foo';
        $s->notFound($notFoundCallback);
    }

    /************************************************
     * HOOKS
     ************************************************/

    /**
     * Test hook listener
     *
     * Pre-conditions:
     * Slim app instantiated;
     * Hook name does not exist;
     * Listeners are callable objects;
     *
     * Post-conditions:
     * Callables are invoked in expected order;
     */
    public function testRegistersAndCallsHooksByPriority()
    {
        $this->expectOutputString('barfoo');
        $app = new \Slim\Slim();
        $callable1 = function () { echo "foo"; };
        $callable2 = function () { echo "bar"; };
        $app->hook('test.hook.one', $callable1); //default is 10
        $app->hook('test.hook.one', $callable2, 8);
        $hooks = $app->getHooks();
        $this->assertEquals(7, count($hooks)); //6 default, 1 custom
        $app->applyHook('test.hook.one');
    }

    /**
     * Test hook listener if listener is not callable
     *
     * Pre-conditions:
     * Slim app instantiated;
     * Hook name does not exist;
     * Listener is NOT a callable object;
     *
     * Post-conditions:
     * Hook is created;
     * Callable is NOT assigned to hook;
     */
    public function testHookInvalidCallable()
    {
        $app = new \Slim\Slim();
        $callable = 'test'; //NOT callable
        $app->hook('test.hook.one', $callable);
        $this->assertEquals(array(array()), $app->getHooks('test.hook.one'));
    }

    /**
     * Test hook invocation if hook does not exist
     *
     * Pre-conditions:
     * Slim app instantiated;
     * Hook name does not exist;
     *
     * Post-conditions:
     * Hook is created;
     * Hook initialized with empty array;
     */
    public function testHookInvocationIfNotExists()
    {
        $app = new \Slim\Slim();
        $app->applyHook('test.hook.one');
        $this->assertEquals(array(array()), $app->getHooks('test.hook.one'));
    }

    /**
     * Test clear hooks
     *
     * Pre-conditions:
     * Slim app instantiated;
     * Two hooks exist, each with one listener;
     *
     * Post-conditions:
     * Case A: Listeners for 'test.hook.one' are cleared;
     * Case B: Listeners for all hooks are cleared;
     */
    public function testHookClear()
    {
        $app = new \Slim\Slim();
        $app->hook('test.hook.one', function () {});
        $app->hook('test.hook.two', function () {});
        $app->clearHooks('test.hook.two');
        $this->assertEquals(array(array()), $app->getHooks('test.hook.two'));
        $hookOne = $app->getHooks('test.hook.one');
        $this->assertTrue(count($hookOne[10]) === 1);
        $app->clearHooks();
        $this->assertEquals(array(array()), $app->getHooks('test.hook.one'));
    }

    /**
     * Test hooks accept multiple arguments
     *
     * Pre-conditions:
     * Slim app instantiated;
     * Hook name does not exist;
     * Listener is a callable object;
     *
     * Post-conditions:
     * Callable invoked with 2 arguments
     */
    public function testHooksMultipleArguments()
    {
        $testArgA = 'argumentA';
        $testArgB = 'argumentB';

        $this->expectOutputString($testArgA . $testArgB);

	$app = new \Slim\Slim();

        $app->hook('test.hook.one', function ($argA, $argB) {
                echo $argA . $argB;
        });
        $app->applyHook('test.hook.one', $testArgA, $testArgB);
    }

    /**
     * Test late static binding
     *
     * Pre-conditions:
     * Slim app is extended by Derived class and instantiated;
     * Derived class overrides the 'getDefaultSettings' function and adds an extra default config value
     * Test that the new config value exists
     *
     * Post-conditions:
     * Config value exists and is equal to expected value
     */
    public function testDerivedClassCanOverrideStaticFunction()
    {
        $app = new Derived();
        $this->assertEquals($app->config("late-static-binding"), true);
    }
}
