<?php

namespace Phroute\Phroute\Dispatcher;

use Phroute\Phroute\RouteCollector;
use Phroute\Phroute\RouteParser;
use Phroute\Phroute\Dispatcher;
use Phroute\Phroute\Route;

class Test {
    
    public function route()
    {
        return 'testRoute';
    }
    
    public function anyIndex()
    {
        return 'testRouteAnyIndex';
    }
    
    public function anyTest()
    {
        return 'testRouteAnyTest';
    }
    
    public function getTest()
    {
        return 'testRouteGetTest';
    }
    
    public function postTest()
    {
        return 'testRoutePostTest';
    }
    
    public function putTest()
    {
        return 'testRoutePutTest';
    }

    public function patchTest()
    {
        return 'testRoutePatchTest';
    }

    public function deleteTest()
    {
        return 'testRouteDeleteTest';
    }
    
    public function headTest()
    {
        return 'testRouteHeadTest';
    }
    
    public function optionsTest()
    {
        return 'testRouteOptionsTest';
    }

    public function getCamelCaseHyphenated()
    {
        return 'hyphenated';
    }

    public function getParameter($param)
    {
        return $param;
    }

    public function getParameterHyphenated($param)
    {
        return $param;
    }

    public function getParameterOptional($param = 'default')
    {
        return $param;
    }

    public function getParameterRequired($param)
    {
        return $param;
    }

    public function getParameterOptionalRequired($param, $param2 = 'default')
    {
        return $param . $param2;
    }
}

class DispatcherTest extends \PHPUnit_Framework_TestCase {

    /**
     * Set appropriate options for the specific Dispatcher class we're testing
     */
    private function router()
    {
        return new RouteCollector(new RouteParser);
    }

    private function dispatch($router, $method, $uri)
    {
        return (new Dispatcher($router->getData()))->dispatch($method, $uri);
    }

    /**
     * @dataProvider provideFoundDispatchCases
     */
    public function testFoundDispatches($method, $uri, $callback, $expected)
    {
        $r = $this->router();
        $callback($r);
        $response = $this->dispatch($r, $method, $uri);
        $this->assertEquals($expected, $response);
    }

    /**
     * @dataProvider provideNotFoundDispatchCases
     * @expectedException \Phroute\Phroute\Exception\HttpRouteNotFoundException
     * @expectedExceptionMessage does not exist
     */
    public function testNotFoundDispatches($method, $uri, $callback)
    {
        $r = $this->router();
        $callback($r);
        $this->dispatch($r, $method, $uri);
    }

    /**
     * @dataProvider provideMethodNotAllowedDispatchCases
     */
    public function testMethodNotAllowedDispatches($method, $uri, $callback, $allowed)
    {
        $this->setExpectedException('\Phroute\Phroute\Exception\HttpMethodNotAllowedException',"Allow: " . implode(', ', $allowed));

        $r = $this->router();
        $callback($r);
        $this->dispatch($r, $method, $uri);
    }
    
    public function testStringObjectIsDispatched()
    {
        $r = $this->router();
        
        $r->addRoute('GET', '/foo', array(__NAMESPACE__.'\\Test','route'));
        
        $response = $this->dispatch($r, 'GET', '/foo');
                
        $this->assertEquals('testRoute',$response);
    }
    
    public function testNamedRoutes()
    {
        $r = $this->router();
        
        $r->addRoute('GET', array('/foo', 'name'), array(__NAMESPACE__.'\\Test','route'));
                
        $this->assertEquals('foo',$r->route('name'));
        
        $r->addRoute('GET', array('/foo/{name}/{something:i}', 'name2'), array(__NAMESPACE__.'\\Test','route'));
                
        $this->assertEquals('foo/joe/something',$r->route('name2', ['joe', 'something']));


        $this->assertTrue($r->hasRoute('name2'));
        $this->assertFalse($r->hasRoute('unknown-name'));
    }

    public function testOptionalReverseRoute()
    {
        $r = $this->router();

        $r->any( array('products/store/{store:i}?', 'products'), array(__NAMESPACE__.'\\Test','route'));

        $this->assertEquals('products/store', $r->route('products'));
        $this->assertEquals('products/store/1', $r->route('products', array(1)));
    }

    public function testReverseRouteWithDashes()
    {
        $r = $this->router();

        $r->any( array('product-catalogue/store/{store:i}?', 'products'), array(__NAMESPACE__.'\\Test','route'));

        $this->assertEquals('product-catalogue/store', $r->route('products'));
        $this->assertEquals('product-catalogue/store/1', $r->route('products', array(1)));
    }

    public function testGroupsReverseRoutes()
    {
        $r = $this->router();

        $r->group([], function($r) {
            $r->any(['products/store/{store:i}?', 'products'], array(__NAMESPACE__.'\\Test','route'));
        });

        $this->assertEquals('products/store', $r->route('products'));
        $this->assertEquals('products/store/1', $r->route('products', array(1)));
    }

    public function testPrefixGroupsReverseRoutes()
    {
        $r = $this->router();

        $r->group(['prefix' => 'product-catalogue/store'], function($r) {
            $r->any(['items/{store:i}?', 'products'], array(__NAMESPACE__.'\\Test','route'));
        });

        $this->assertEquals('product-catalogue/store/items', $r->route('products'));
        $this->assertEquals('product-catalogue/store/items/1', $r->route('products', array(1)));
    }

    /**
     * @expectedException \Phroute\Phroute\Exception\BadRouteException
     * @expectedExceptionMessage Expecting route variable 'store'
     */
    public function testMissingParameterReverseRoute()
    {
        $r = $this->router();

        $r->any( array('products/store/{store:i}', 'products'), array(__NAMESPACE__.'\\Test','route'));

        $this->assertEquals('products/store', $r->route('products'));
    }

    /**
     * @expectedException \Phroute\Phroute\Exception\BadRouteException
     * @expectedExceptionMessage Cannot use the same placeholder 'test' twice
     */
    public function testDuplicateVariableNameError()
    {
        $this->router()->addRoute('GET', '/foo/{test}/{test:\d+}', function() {
            
        });
    }

    /**
     * @expectedException \Phroute\Phroute\Exception\BadRouteException
     * @expectedExceptionMessage Cannot register two routes matching 'user/([^/]+)' for method 'GET'
     */
    public function testDuplicateVariableRoute()
    {
        $r = $this->router();
        $r->addRoute('GET', '/user/{id}', function() {
            
        }); // oops, forgot \d+ restriction ;)
        $r->addRoute('GET', '/user/{name}', function() {
            
        });
    }

    /**
     * @expectedException \Phroute\Phroute\Exception\BadRouteException
     * @expectedExceptionMessage Cannot register two routes matching 'user' for method 'GET'
     */
    public function testDuplicateStaticRoute()
    {
        $r = $this->router();
        $r->addRoute('GET', '/user', function() {
            
        });
        $r->addRoute('GET', '/user', function() {
            
        });
    }

    /**
     * @expectedException \Phroute\Phroute\Exception\BadRouteException
     * @expectedExceptionMessage Static route 'user/nikic' is shadowed by previously defined variable route 'user/([^/]+)' for method 'GET'
     */
    public function testShadowedStaticRoute()
    {
        $r = $this->router();
        $r->addRoute('GET', '/user/{name}', function() {
            
        });
        $r->addRoute('GET', '/user/nikic', function() {
            
        });
    }

    public function testBeforeFilters()
    {
        $r = $this->router();

        $dispatchedFilter = false;
        
        $r->filter('test', function() use(&$dispatchedFilter){
            $dispatchedFilter = true;
        });

        $r->addRoute('GET', '/user', function() {
            return 'dispatched';
        }, array('before' => 'test'));

        $this->assertEquals('dispatched', $this->dispatch($r, 'GET', '/user'));
        
        $this->assertTrue($dispatchedFilter);
    }
    
    public function testBeforeFiltersStringClass()
    {
        $r = $this->router();
        
        $r->filter('test', array(__NAMESPACE__ . '\Test','route'));

        $r->addRoute('GET', '/user', function() {}, array('before' => 'test'));

        $this->assertEquals('testRoute', $this->dispatch($r, 'GET', '/user'));
    }

    public function testBeforeFilterCancels()
    {
        $r = $this->router();
        
        $r->filter('test', function(){            
            return 'cancel';
        });

        $r->addRoute('GET', '/user', function() {
            return 'dispatched';
        }, array('before' => 'test'));

        $this->assertEquals('cancel', $this->dispatch($r, 'GET', '/user'));
    }
    
    
    public function testAfterFilters()
    {
        $r = $this->router();

        $dispatchedFilter = false;
        
        $r->filter('test', function($response) use(&$dispatchedFilter){
            $dispatchedFilter = true;
            
            return $response . ' filtered';
        });

        $r->addRoute('GET', '/user', function() {
            return 'test';
        }, array('after' => 'test'));

        $response = $this->dispatch($r, 'GET', '/user');
        
        $this->assertTrue($dispatchedFilter);
        
        $this->assertEquals('test filtered', $response);
    }
    
    public function testFilterGroups()
    {
        $r = $this->router();
        
        $dispatchedFilter = 0;
        $dispatchedFilter2 = 0;
        
        $r->filter('test', function() use(&$dispatchedFilter){
            $dispatchedFilter++;
        });
        
        $r->filter('test2', function() use(&$dispatchedFilter2){
            $dispatchedFilter2++;
        });
        
        $r->group(array('before' => 'test'), function($router){
            $router->addRoute('GET', '/user', function() {
            
            });
            $router->group(array('before' => 'test2'), function($router){
                $router->addRoute('GET', '/user2', function() {

                });
            });
        });
        
        $this->dispatch($r, 'GET', '/user');
        
        $this->assertEquals(1, $dispatchedFilter);
        
        $this->dispatch($r, 'GET', '/user2');
        
        $this->assertEquals(2, $dispatchedFilter);
        $this->assertEquals(1, $dispatchedFilter2);
        
    }

    public function testMultiplePrefixedGroups()
    {
        $r = $this->router();

        $r->group(['prefix' => '/user'], function($router){
            $router->addRoute('GET', '/', function() {

            });
            $router->group(['prefix' => '/foo'], function($router){
                $router->addRoute('GET', '/{id}', function() {

                });
            });
        });

        $this->dispatch($r, 'GET', '/user');

        $this->dispatch($r, 'GET', '/user/foo/2');

    }

    public function testVariablePrefix()
    {
        $r = $this->router();
        $r->group(['prefix' => '/demo/{variable}'], function($router){
            $router->addRoute('GET', '/something', function($var){
               return $var;
            });

            $router->addRoute('GET', '/something-else', function($var){
                return $var;
            });
        });

        $response = $this->dispatch($r, 'GET', '/demo/2/something');

        $this->assertEquals('2', $response);

        $response = $this->dispatch($r, 'GET', '/demo/4/something-else');

        $this->assertEquals('4', $response);
    }

    public function testValidMethods()
    {
        $this->assertEquals(array(
            Route::ANY,
            Route::GET,
            Route::POST,
            Route::PUT,
            Route::PATCH,
            Route::DELETE,
            Route::HEAD,
            Route::OPTIONS,
        ), $this->router()->getValidMethods());
    }

    public function testAnyRespondsToDeletePutAndGet()
    {
        $r = $this->router();

        $r->any('/user', function(){
            return 'yes';
        });

        $this->assertEquals('yes', $this->dispatch($r, Route::GET, 'user'));
        $this->assertEquals('yes', $this->dispatch($r, Route::DELETE, 'user'));
        $this->assertEquals('yes', $this->dispatch($r, Route::PUT, 'user'));
        $this->assertEquals('yes', $this->dispatch($r, Route::PATCH, 'user'));
        $this->assertEquals('yes', $this->dispatch($r, Route::POST, 'user'));
        $this->assertEquals('yes', $this->dispatch($r, Route::OPTIONS, 'user'));
        $this->assertEquals('yes', $this->dispatch($r, 'MADE_UP_NON_STANDARD_METHOD', 'user'));
    }
    
    public function testRestfulControllerMethods()
    {

        $r = $this->router();

        $r->controller('/user', __NAMESPACE__ . '\\Test');

        $data = $r->getData();

        $this->assertEquals($r->getValidMethods(), array_keys($data->getStaticRoutes()['user/test']));

        $this->assertEquals(array(Route::ANY), array_keys($data->getStaticRoutes()['user']));
        $this->assertEquals(array(Route::ANY), array_keys($data->getStaticRoutes()['user/index']));

        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::GET, 'user'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::POST, 'user'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::PUT, 'user'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::PATCH, 'user'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::DELETE, 'user'));

        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::GET, 'user/index'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::POST, 'user/index'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::PUT, 'user/index'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::PATCH, 'user/index'));
        $this->assertEquals('testRouteAnyIndex', $this->dispatch($r, Route::DELETE, 'user/index'));

        $this->assertEquals('hyphenated', $this->dispatch($r, Route::GET, 'user/camel-case-hyphenated'));

        $this->assertEquals('joe', $this->dispatch($r, Route::GET, 'user/parameter/joe'));
        $this->assertEquals('joe', $this->dispatch($r, Route::GET, 'user/parameter-hyphenated/joe'));

        $this->assertEquals('joe', $this->dispatch($r, Route::GET, 'user/parameter-optional/joe'));
        $this->assertEquals('default', $this->dispatch($r, Route::GET, 'user/parameter-optional'));
        $this->assertEquals('joedefault', $this->dispatch($r, Route::GET, 'user/parameter-optional-required/joe'));
        $this->assertEquals('joegreen', $this->dispatch($r, Route::GET, 'user/parameter-optional-required/joe/green'));
    }


    /**
     * @expectedException \Phroute\Phroute\Exception\HttpRouteNotFoundException
     * @expectedExceptionMessage does not exist
     */
    public function testRestfulOptionalRequiredControllerMethodThrows()
    {
        $r = $this->router();

        $r->controller('/user', __NAMESPACE__ . '\\Test');

        $this->dispatch($r, Route::GET, 'user/parameter-optional-required');
    }

    /**
     * @expectedException \Phroute\Phroute\Exception\HttpRouteNotFoundException
     * @expectedExceptionMessage does not exist
     */
    public function testRestfulRequiredControllerMethodThrows()
    {
        $r = $this->router();

        $r->controller('/user', __NAMESPACE__ . '\\Test');

        $this->dispatch($r, Route::GET, 'user/parameter-required');
    }

    /**
     * @expectedException \Phroute\Phroute\Exception\HttpRouteNotFoundException
     * @expectedExceptionMessage does not exist
     */
    public function testRestfulHyphenateControllerMethodThrows()
    {
        $r = $this->router();

        $r->controller('/user', __NAMESPACE__ . '\\Test');

        $this->dispatch($r, Route::GET, 'user/camelcasehyphenated');
    }

    public function testHyphenatedRoutes()
    {
        $r = $this->router();

        $r->get('/user-name', function(){
            return 'test';
        });

        $val = $this->dispatch($r, Route::GET, '/user-name');

        $this->assertEquals('test', $val);
    }

    /**
     * @dataProvider characterfulRoutes
     */
    public function testCharacterDynamicRoutes($route, $uri, $expected)
    {
        $r = $this->router();

        $r->get($route, function(){
            return implode(' ', func_get_args());
        });

        $val = $this->dispatch($r, Route::GET, $uri);

        $this->assertEquals($expected, $val);
    }

    public function characterfulRoutes()
    {
        return array(
            // route / dispatch URI / expected
            array('/user-name/{name}', '/user-name/joe', 'joe'),
            array('/user_name/{name}', '/user_name/joe', 'joe'),
            array('/user+++name/{name}', '/user+++name/joe', 'joe'),
            array('/user.name/{name_with_undersc0r3s_n_nums}', '/user.name/joe', 'joe'),
            array('/us;er:nam;e/{name}', '/us;er:nam;e/joe', 'joe'),
            array('/us%25r:na%20;e/{name}', '/us%25r:na%20;e/joe', 'joe'),

            // Regex characters to check for proper escaping
            array('/sta++++tic1', '/sta++++tic1', ''),

            array('/static1/{name}/static2/{country}', '/static1/joe/static2/uk', 'joe uk'),
        );
    }

    public function testRestfulMethods()
    {
        
        $r = $this->router();
        
        $methods = $r->getValidMethods();
        
        foreach($methods as $method)
        {
            $r->$method('/user','callback');
        }
        
        $data = $r->getData();
        
        $this->assertEquals($methods, array_keys($data->getStaticRoutes()['user']));
    }
    
    public function provideFoundDispatchCases()
    {
        $cases = [];
        
         // 0 -------------------------------------------------------------------------------------->

        $callback = function($r) {
            $r->addRoute('GET', '/', function() {
                return true;
            });
        };
        
        $cases[] = ['GET', '', $callback, true];

        $cases[] = ['GET', '/', $callback, true];
        
        
        $callback = function($r) {
            $r->addRoute('GET', '', function() {
                return true;
            });
        };
        
        $cases[] = ['GET', '', $callback, true];

        $cases[] = ['GET', '/', $callback, true];

        // 0 -------------------------------------------------------------------------------------->

        $callback = function($r) {
            $r->addRoute('GET', '/resource/123/456', function() {
                return true;
            });
        };

        $cases[] = ['GET', '/resource/123/456', $callback, true];
        
        
        
        $callback = function($r) {
            $r->addRoute('GET', 'resource/123/456', function() {
                return true;
            });
        };

        $cases[] = ['GET', 'resource/123/456', $callback, true];
        
        
        $callback = function($r) {
            $r->addRoute('GET', 'resource/123/456', function() {
                return true;
            });
        };

        $cases[] = ['GET', '/resource/123/456', $callback, true];

        // 1 -------------------------------------------------------------------------------------->

        $callback = function($r) {
            $r->addRoute('GET', '/handler0', function() {
                
            });
            $r->addRoute('GET', '/handler1', function() {
                
            });
            $r->addRoute('GET', '/handler2', function() {
                return true;
            });
        };
        $cases[] = ['GET', '/handler2', $callback, true];

        // 2 -------------------------------------------------------------------------------------->

        $callback = function($r) {
            $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', function($name, $id) {
                return [$name, $id];
            });
            $r->addRoute('GET', '/user/{id:[0-9]+}', function($id) {
                return $id;
            });
            $r->addRoute('GET', '/user/{name}', function($name) {
                return $name;
            });
        };

        $cases[] = ['GET', '/user/rdlowrey', $callback, 'rdlowrey'];


        // 3 -------------------------------------------------------------------------------------->
        // reuse $callback from #2

        $cases[] = ['GET', '/user/12345', $callback, '12345'];

        // 4 -------------------------------------------------------------------------------------->
        // reuse $callback from #3

        $cases[] = ['GET', '/user/NaN', $callback, 'NaN'];

        // 5 -------------------------------------------------------------------------------------->
        // reuse $callback from #4
        $cases[] = ['GET', '/user/rdlowrey/12345', $callback, ['rdlowrey', '12345']];

        // 6 -------------------------------------------------------------------------------------->

        $callback = function( $r) {
            $r->addRoute('GET', '/user/{id:[0-9]+}', function() {
                
            });
            $r->addRoute('GET', '/user/12345/extension', function() {
                
            });
            $r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', function($id, $extension) {
                return [$id, $extension];
            });
        };

        $cases[] = ['GET', '/user/12345.svg', $callback, ['12345', 'svg']];

        // 7 ----- Test GET method fallback on HEAD route miss ------------------------------------>

        $callback = function($r) {
            $r->addRoute('GET', '/user/{name}', function($name) {
                return $name;
            });
            $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', function($name, $id) {
                return [$name, $id];
            });
            $r->addRoute('GET', '/static0', function() {
                return 'static0';
            });
            $r->addRoute('GET', '/static1', function() {
                
            });
            $r->addRoute('HEAD', '/static1', function() {
                return 'static1head';
            });
        };

        $cases[] = ['HEAD', '/user/rdlowrey', $callback, 'rdlowrey'];

        // 8 ----- Test GET method fallback on HEAD route miss ------------------------------------>
        // reuse $callback from #7
        $cases[] = ['HEAD', '/user/rdlowrey/1234', $callback, ['rdlowrey', '1234']];

        // 9 ----- Test GET method fallback on HEAD route miss ------------------------------------>
        // reuse $callback from #8

        $cases[] = ['HEAD', '/static0', $callback, 'static0'];

        // 10 ---- Test existing HEAD route used if available (no fallback) ----------------------->
        // reuse $callback from #9

        $cases[] = ['HEAD', '/static1', $callback, 'static1head'];

        // x -------------------------------------------------------------------------------------->

        
        // 11 -------------------------------------------------------------------------------------->
        // Test optional parameter
        $callback = function($r) {
            $r->addRoute('GET', '/user/{name}/{id:[0-9]+}?', function($name, $id = null) {
                return [$name, $id];
            });
        };

        $cases[] = ['GET', '/user/rdlowrey', $callback, array('rdlowrey', null)];
        
        // 12
        $cases[] = ['GET', '/user/rdlowrey/23', $callback, array('rdlowrey', '23')];
        
        // 13 -------------------------------------------------------------------------------------->
        // Test multiple optional parameters
        $callback = function($r) {
            $r->addRoute('GET', '/user/{name}/{id:[0-9]+}?/{other}?', function($name, $id = null, $other = null) {
                return [$name, $id, $other];
            });
        };

        $cases[] = ['GET', '/user/rdlowrey', $callback, array('rdlowrey', null, null)];
        
        // 14
        $cases[] = ['GET', '/user/rdlowrey/23', $callback, array('rdlowrey', '23', null)];
        
        //15
        $cases[] = ['GET', '/user/rdlowrey/23/blah', $callback, array('rdlowrey', '23', 'blah')];
        

        $callback = function($r) {
            $r->addRoute('GET', '/user/random_{name}', function($name) {
                return $name;
            });
        };

        //16
        $cases[] = ['GET', '/user/random_rdlowrey', $callback, 'rdlowrey'];
        
        
        $callback = function($r) {
            $r->addRoute('GET', '/user/random_{name}?', function($name = null) {
                return $name;
            });
        };

        //17
        $cases[] = ['GET', '/user/random_rdlowrey', $callback, 'rdlowrey'];
         //18
        $cases[] = ['GET', '/user/random_', $callback, null];
        
        $callback = function($r) {
            $r->addRoute('GET', '{name}?', function($name = null) {
                return $name;
            });
        };

        //19
        $cases[] = ['GET', 'rdlowrey', $callback, 'rdlowrey'];
         //20
        $cases[] = ['GET', '/', $callback, null];
        
        // 11 -------------------------------------------------------------------------------------->
        // Test shortcuts parameter
        $callback = function($r) {
            $r->addRoute('GET', '/user/{id:i}', function($id) {
                return $id;
            });
            $r->addRoute('GET', '/user1/{idname:a}', function($idname) {
                return array($idname);
            });
            $r->addRoute('GET', '/user2/{hexcode:h}', function($hexcode) {
                return array($hexcode);
            });
            $r->addRoute('GET', '/user3/{id:i}/{hexcode:h}?', function($id, $hexcode = null) {
                return array($id, $hexcode);
            });
            $r->addRoute('GET', '/user4/{slug:c}', function($slug) {
                return array($slug);
            });
        };

        $cases[] = ['GET', '/user/21', $callback, '21'];
        $cases[] = ['GET', '/user1/abcdezzASd123', $callback, array('abcdezzASd123')];
        $cases[] = ['GET', '/user2/abcde123', $callback, array('abcde123')];
        $cases[] = ['GET', '/user3/21/abcde123', $callback, array('21','abcde123')];
        $cases[] = ['GET', '/user3/21', $callback, array('21', null)];
        $cases[] = ['GET', '/user4/test_something-123', $callback, array('test_something-123')];
        
        
        // 11 -------------------------------------------------------------------------------------->
        // Test shortcuts parameter
        $callback = function($r) {
            $r->addRoute('GET', '/user/{id}?/{id2}?/{id3}?', function() {
                return 'first';
            });
            $r->addRoute('GET', '/user2/{id}?', function() {
                return 'second';
            });
            $r->addRoute('GET', '/user3/{id}?', function() {
                return 'third';
            });
            $r->addRoute('GET', '/user4/{id}?/{id2}?/{id3}?', function() {
                return 'fourth';
            });
        };

        $cases[] = ['GET', '/user/21', $callback, 'first'];
        $cases[] = ['GET', '/user2/abcdezzASd123', $callback, 'second'];
        $cases[] = ['GET', '/user2/abcde123', $callback, 'second'];
        $cases[] = ['GET', '/user/21/abcde123', $callback, 'first'];
        $cases[] = ['GET', '/user2/21', $callback, 'second'];
        $cases[] = ['GET', '/user3/abcdezzASd123', $callback, 'third'];
        $cases[] = ['GET', '/user3/abcde123', $callback, 'third'];
        $cases[] = ['GET', '/user3/21', $callback, 'third'];
        $cases[] = ['GET', '/user4/abcdezzASd123', $callback, 'fourth'];
        $cases[] = ['GET', '/user4/abcde123', $callback, 'fourth'];
        $cases[] = ['GET', '/user4/21', $callback, 'fourth'];
        
        // 11 -------------------------------------------------------------------------------------->
        // Test shortcuts parameter
        $callback = function($r) {
            $r->addRoute('GET', 'ext/{asset}.json', function($asset) {
                return $asset . ' jsonencoded';
            });
            $r->addRoute('GET', 'ext/{asset}', function($asset) {
                return $asset;
            });
        };

        $cases[] = ['GET', 'ext/asset', $callback, 'asset'];
        $cases[] = ['GET', 'ext/asset.json', $callback, 'asset jsonencoded'];

        // 12 -------------------------------------------------------------------------------------->
        // Test \d{3,4} style quantifiers
        $callback = function($r) {
            $r->addRoute('GET', 'server/{ip:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}}/{name}/{newname}?', function($ip, $name, $newname = null) {
                return trim("$ip $name $newname");
            });
        };

        $cases[] = ['GET', 'server/10.10.10.10/server1', $callback, '10.10.10.10 server1'];
        $cases[] = ['GET', 'server/0.0.0.0/server2', $callback, '0.0.0.0 server2'];
        $cases[] = ['GET', 'server/123.2.23.111/server3/server4', $callback, '123.2.23.111 server3 server4'];

        // 13 -------------------------------------------------------------------------------------->
        // Test \d{3,4} style quantifiers
        $callback = function($r) {
            $r->addRoute('GET', 'date/{year:\d{4}}/{month:\d+}?', function($year, $month) {
                return trim("$year $month");
            });
        };

        $cases[] = ['GET', 'date/1990/05', $callback, '1990 05'];

        // 14 -------------------------------------------------------------------------------------->
        // Test \d{3,4} style quantifiers
        $callback = function($r) {
            $r->addRoute('GET', 'date/{year:\d{4}}/{month:\d{2}}', function($year, $month) {
                return trim("$year $month");
            });
        };

        $cases[] = ['GET', 'date/2010/06', $callback, '2010 06'];

        return $cases;
    }

    public function provideNotFoundDispatchCases()
    {
        $cases = [];

        // 0 -------------------------------------------------------------------------------------->

        $callback = function( $r) {
            $r->addRoute('GET', '/resource/123/456', 'handler0');
        };

        $method = 'GET';
        $uri = '/not-found';

        $cases[] = [$method, $uri, $callback];

        // 1 -------------------------------------------------------------------------------------->
        // reuse callback from #0
        $method = 'POST';
        $uri = '/not-found';

        $cases[] = [$method, $uri, $callback];

        // 2 -------------------------------------------------------------------------------------->
        // reuse callback from #1
        $method = 'PUT';
        $uri = '/not-found';

        $cases[] = [$method, $uri, $callback];

        // 3 -------------------------------------------------------------------------------------->

        $callback = function( $r) {
            $r->addRoute('GET', '/handler0', 'handler0');
            $r->addRoute('GET', '/handler1', 'handler1');
            $r->addRoute('GET', '/handler2', 'handler2');
        };

        $method = 'GET';
        $uri = '/not-found';

        $cases[] = [$method, $uri, $callback];

        // 4 -------------------------------------------------------------------------------------->

        $callback = function( $r) {
            $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
            $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
            $r->addRoute('GET', '/user/{name}', 'handler2');
        };

        $method = 'GET';
        $uri = '/not-found';

        $cases[] = [$method, $uri, $callback];

        // 5 -------------------------------------------------------------------------------------->
        // reuse callback from #4
        $method = 'GET';
        $uri = '/user/rdlowrey/12345/not-found';

        $cases[] = [$method, $uri, $callback];

        // x -------------------------------------------------------------------------------------->

        $callback = function($r) {
            $r->addRoute('GET', '/user/random_{name}?', function($name = null) {
                return $name;
            });
        };

        //17
        $cases[] = ['GET', 'rdlowrey', $callback];
        
        //19
        $cases[] = ['GET', '/user/rdlowrey', $callback, null];


        // 20 -------------------------------------------------------------------------------------->
        // Test \d{3,4} style quantifiers
        $callback = function($r) {
            $r->addRoute('GET', 'server/{ip:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}}', function($year) {
                return trim("$year");
            });
        };

        $cases[] = ['GET', 'server/1044.10.10.10', $callback, null];
        $cases[] = ['GET', 'server/0.0.0', $callback, null];
        $cases[] = ['GET', 'server/.2.23.111', $callback, null];
        
        return $cases;
    }

    public function provideMethodNotAllowedDispatchCases()
    {
        $cases = [];

        // 0 -------------------------------------------------------------------------------------->

        $callback = function( $r) {
            $r->addRoute('GET', '/resource/123/456', 'handler0');
        };

        $method = 'POST';
        $uri = '/resource/123/456';
        $allowedMethods = ['GET'];

        $cases[] = [$method, $uri, $callback, $allowedMethods];

        // 1 -------------------------------------------------------------------------------------->

        $callback = function( $r) {
            $r->addRoute('GET', '/resource/123/456', 'handler0');
            $r->addRoute('POST', '/resource/123/456', 'handler1');
            $r->addRoute('PUT', '/resource/123/456', 'handler2');
        };

        $method = 'DELETE';
        $uri = '/resource/123/456';
        $allowedMethods = ['GET', 'POST', 'PUT'];

        $cases[] = [$method, $uri, $callback, $allowedMethods];

        // 2 -------------------------------------------------------------------------------------->

        $callback = function( $r) {
            $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
            $r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1');
            $r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2');
            $r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3');
        };

        $method = 'DELETE';
        $uri = '/user/rdlowrey/42';
        $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH'];

        $cases[] = [$method, $uri, $callback, $allowedMethods];

        // x -------------------------------------------------------------------------------------->

        return $cases;
    }

}