<?php
/**
 * core.framework
 *
 * @category   Core
 * @package    Core_Router
 * @subpackage UnitTests
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */

/**
 * @category   Core
 * @package    Core_Router
 * @subpackage UnitTests
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 * @group      Core_Router
 */
class Core_RouterTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var Core_Router
     */
    protected $_object;

    protected function setUp()
    {
        $this->_object = new Core_Router;
    }

    protected function tearDown()
    {
        Core_Application::resetInstance();
    }

    /**
     * @covers Core_Router::setOptions
     */
    public function testCanSetOptions()
    {
        $this->_object->setOptions(array('separator' => '#'));
        $this->assertEquals('#', $this->_object->getSeparator());
    }

    /**
     * @covers Core_Router::setConfig
     */
    public function testCanSetConfig()
    {
        $config = $this->getMock('Zend_Config', null, array(array('separator' => '#')));
        $this->_object->setConfig($config);
        $this->assertEquals('#', $this->_object->getSeparator());
    }

    /**
     * @covers Core_Router::__construct
     */
    public function testCanSetOptionsViaConstructor()
    {
        $object  = new Core_Router(array('separator' => '#'));
        $this->assertEquals('#', $object->getSeparator());
    }

    /**
     * @covers Core_Router::__construct
     */
    public function testCanSetConfigViaConstructor()
    {
        $config = $this->getMock('Zend_Config', null, array(array('separator' => '#')));
        $object = new Core_Router($config);
        $this->assertEquals('#', $object->getSeparator());
    }

    /**
     * @covers Core_Router::setOptions
     */
    public function testPassingAnUnknownOptionsThrowsAnInvalidArgumentException()
    {
        $this->setExpectedException('InvalidArgumentException');
        $this->_object->setOptions(array('no-such' => 'option'));
    }

    /**
     * @covers Core_Router::setRequest
     * @covers Core_Router::getRequest
     */
    public function testCanSetAndGetRequest()
    {
        $request = $this->getMock('Core_Request');
        $this->_object->setRequest($request);
        $this->assertSame($request, $this->_object->getRequest());
    }

    /**
     * @covers Core_Router::getRequest
     */
    public function testIfNoRequestIsSetGetRequestWillPullItFromApplicationInstanceAsRequest()
    {
        $request   = $this->getMock('Core_Request');
        $container = $this->_mockContainer('Request', $request);
        Core_Application::getInstance()->setContainer($container);
        $this->assertSame($request, $this->_object->getRequest());
    }

    /**
     * @covers Core_Router::setPluginLoader
     * @covers Core_Router::getPluginLoader
     */
    public function testCanSetAndGetPluginLoader()
    {
        $pluginLoader = $this->getMock('Core_Loader_PluginLoader');
        $this->_object->setPluginLoader($pluginLoader);
        $this->assertSame($pluginLoader, $this->_object->getPluginLoader());
    }

    /**
     * @covers Core_Router::getPluginLoader
     */
    public function testIfNoRequestIsSetGetRequestWillPullItFromApplicationInstanceAsPluginLoader()
    {
        $pluginLoader = $this->getMock('Core_Loader_PluginLoader');
        $container    = $this->_mockContainer('PluginLoader', $pluginLoader);
        Core_Application::getInstance()->setContainer($container);
        $this->assertSame($pluginLoader, $this->_object->getPluginLoader());
    }

    /**
     * @covers Core_Router::setCurrentRoute
     * @covers Core_Router::getCurrentRoute
     */
    public function testCanSetAndGetCurrentRoute()
    {
        $route = $this->getMockForAbstractClass('Core_Router_Route_Abstract');
        $this->_object->setCurrentRoute($route);
        $this->assertSame($route, $this->_object->getCurrentRoute());
    }

    /**
     * @covers Core_Router::resetCurrentRoute
     */
    public function testCanResetCurrentRoute()
    {
        $route = $this->getMockForAbstractClass('Core_Router_Route_Abstract');
        $this->_object
            ->setCurrentRoute($route)
            ->resetCurrentRoute();

        $this->assertNull($this->_object->getCurrentRoute());
    }

    /**
     * @covers Core_Router::setSeparator
     * @covers Core_Router::getSeparator
     */
    public function testCanSetAndGetSeparator()
    {
        $this->_object->setSeparator('#');
        $this->assertEquals('#', $this->_object->getSeparator());
    }

    /**
     * @covers Core_Router::getSeparator
     */
    public function testDefaultSeparatorIsASlash()
    {
        $this->assertEquals('/', $this->_object->getSeparator());
    }

    /**
     * @covers Core_Router::hasRoute
     */
    public function testCanTestIfRouteDefinedByName()
    {
        $route = $this->_mockRoute('name');
        $route
            ->expects($this->once())
            ->method('getName')
            ->will($this->returnValue('name'));

        $this->_object->addRoute($route);
        $this->assertTrue($this->_object->hasRoute('name'));
    }

    /**
     * @covers Core_Router::addRoute
     */
    public function testCanAddRouteInstance()
    {
        $route = $this->_mockRoute('name');
        $this->_object->addRoute($route);

        $this->assertContains($route, $this->_object->getRoutes());
    }


    /**
     * @covers Core_Router::addRoute
     */
    public function testWillSetSelfToRouteInstanceWhileAdding()
    {
        $route = $this->_mockRoute('name', array('setRouter'));
        $route
            ->expects($this->once())
            ->method('setRouter')
            ->with($this->equalTo($this->_object));

        $this->_object->addRoute($route);
    }

    /**
     * @covers Core_Router::addRoutes
     * @depends testCanAddRouteInstance
     */
    public function testCanAddRoutesAsArrayOfInstances()
    {
        $route = $this->_mockRoute('name');
        $this->_object->addRoutes(array($route));

        $this->assertTrue($this->_object->hasRoute('name'));
    }

    /**
     * @covers Core_Router::addRoutes
     */
    public function testWhenAddingMultipleRoutesViaAddRoutesTheRouteNameWillBePulledFromTheInstance()
    {
        $route = $this->_mockRoute('instanceName');

        $this->_object->addRoutes(array('keyName' => $route));
        $this->assertTrue($this->_object->hasRoute('instanceName'));
        $this->assertFalse($this->_object->hasRoute('keyName'));
    }

    /**
     * @covers Core_Router::createRoute
     */
    public function testCanCreateRoute()
    {
        $this->_object
            ->setPluginLoader($this->_mockPluginLoader('myRouteName', $this->_mockRoute('myRouteName')));

        $this->_object->createRoute('type', 'myRouteName', array('opt' => 'ions'));
    }

    /**
     * @covers Core_Router::createRoute
     */
    public function testCreatingARouteWillAddIt()
    {
        $this->_object
            ->setPluginLoader($this->_mockPluginLoader('myRouteName', $this->_mockRoute('myRouteName')));
        $this->_object->createRoute('type', 'myRouteName', array('opt' => 'ions'));

        $this->assertTrue($this->_object->hasRoute('myRouteName'));
    }

    /**
     * @covers Core_Router::addRoutes
     */
    public function testCanAddRouteAsArrayOfSpecs()
    {
        // the spec
        $routeSpecs = array(
            'first' => array(
                'type'    => 'type',
                'options' => array('opt' => 'ions')
            ),
        );
        $this->_object
            ->setPluginLoader($this->_mockPluginLoader('first', $this->_mockRoute('first'), 'Type'));

        $this->_object->addRoutes($routeSpecs);
    }

    /**
     * @covers Core_Router::addRoutes
     */
    public function testWhenAddingRoutesIfTypeNotSetWillThrowException()
    {
        $this->setExpectedException('InvalidArgumentException');

        // the spec
        $routeSpecs = array(
            'first' => array(),
        );

        $this->_object->addRoutes($routeSpecs);
    }

    /**
     * @covers Core_Router::addRoutes
     */
    public function testWhenAddingRoutesIfOptionsNotSetWillPassNull()
    {
        // the spec
        $routeSpecs = array(
            'first' => array(
                'type'    => 'static',
            ),
        );
        $this->_object
            ->setPluginLoader($this->_mockPluginLoader('first', $this->_mockRoute('first'), 'Static', null));

        $this->_object->addRoutes($routeSpecs);
    }

    /**
     * @covers Core_Router::getRoute
     */
    public function testCanGetRouteByName()
    {
        $route = $this->_mockRoute('name');
        $this->_object->addRoute($route);

        $this->assertSame($route, $this->_object->getRoute('name'));
    }

    /**
     * @covers Core_Router::getRoute
     */
    public function testFetchingRouteByUnknownNameThrowsAnInvalidArgumentException()
    {
        $this->setExpectedException('InvalidArgumentException');
        $this->_object->getRoute('no.such.route');
    }

    /**
     * @covers Core_Router::getRoutes
     */
    public function testCanGetAllRoutes()
    {
        $route = $this->_mockRoute('route');
        $this->_object->addRoutes(array($route));

        $this->assertContains($route, $this->_object->getRoutes());
    }

    /**
     * @covers Core_Router::getRoutes
     */
    public function testGettingAllRoutesIfNoneDefinedThrowsAnLogicException()
    {
        $this->setExpectedException('LogicException');
        $this->_object->getRoutes();
    }

    /**
     * @covers Core_Router::removeRoute
     */
    public function testCanRemoveRoute()
    {
        $route = $this->_mockRoute('name');
        $this->_object->addRoute($route);
        $this->_object->removeRoute('name');

        $this->assertFalse($this->_object->hasRoute('name'));
    }

    /**
     * @covers Core_Router::removeRoute
     */
    public function testRemovingANonexistantRouteThrowsAnInvalidArgumentException()
    {
        $this->setExpectedException('InvalidArgumentException');
        $this->_object->removeRoute('no.such.route');
    }

    /**
     * @covers Core_Router::setOptions
     */
    public function testCanAddRoutesViaOptions()
    {
        $this->_object->setOptions(array('routes' => array($this->_mockRoute('name'))));

        $this->assertTrue($this->_object->hasRoute('name'));
    }

    /**
     * @covers Core_Router::setOptions
     */
    public function testSettingRequestPluginLoaderCurrentRouteAndOptionsViaOptionsWillBeSkipped()
    {
        // false options
        $options = array(
            'request'      => false,
            'pluginLoader' => false,
            'currentRoute' => false,
        );

        $request      = $this->getMock('Core_Request');
        $pluginLoader = $this->getMock('Core_Loader_PluginLoader');
        $currentRoute = $this->getMockForAbstractClass('Core_Router_Route_Abstract');
        $this->_object
            ->setRequest($request)
            ->setPluginLoader($pluginLoader)
            ->setCurrentRoute($currentRoute)
            ->setOptions($options);

        // although we've tried to set forbidden options, they've been skipped
        $this
            ->assertSame($request, $this->_object->getRequest());
        $this
            ->assertSame($pluginLoader, $this->_object->getPluginLoader());
        $this
            ->assertSame($currentRoute, $this->_object->getCurrentRoute());
    }

    /**
     * @covers Core_Router::route
     */
    public function testNoMatchesWillThrowAnNotFoundException()
    {
        $route = $this->_mockRoute('name', array('match'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with($this->equalTo('/one/two'))

            // "I do NOT match this path!"
            ->will($this->returnValue(false));

        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->addRoute($route);

        $this->setExpectedException('Core_Application_NotFoundException');
        $this->_object->route('/one/two');
    }

    /**
     * @covers Core_Router::route
     */
    public function testRoutingWillStripQueryStringFromPath()
    {
        $route = $route = $this->_mockRoute('name', array('match'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with($this->equalTo('/one/two'))
            ->will($this->returnValue(true));
        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->addRoute($route);

        $this->assertSame($route, $this->_object->route('/one/two?some=param'));
    }

    /**
     * @covers Core_Router::route
     */
    public function testCanPassMethodToRoute()
    {
        $route = $route = $this->_mockRoute('name', array('match'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with($this->equalTo('/one/two'), $this->equalTo('/'), $this->equalTo('PUT'))
            ->will($this->returnValue(true));
        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->addRoute($route);

        $this->assertSame($route, $this->_object->route('/one/two', 'PUT'));
    }

    /**
     * @covers Core_Router::route
     */
    public function testCanRouteCompleteURL()
    {
        $route = $route = $this->_mockRoute('name', array('match'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with(
                $this->equalTo('/one/two'),
                $this->equalTo('/'),
                $this->equalTo(null),
                $this->equalTo('http://www.example.com')
            )
            ->will($this->returnValue(true));
        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->addRoute($route);

        $this->assertSame($route, $this->_object->route('http://www.example.com/one/two?some=param'));
    }

    /**
     * @covers Core_Router::route
     */
    public function testCanRouteCompleteURLWithPort()
    {
        $route = $route = $this->_mockRoute('name', array('match'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with(
                $this->equalTo('/one/two'),
                $this->equalTo('/'),
                $this->equalTo(null),
                $this->equalTo('http://www.example.com:9999')
            )
            ->will($this->returnValue(true));
        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->addRoute($route);

        $this->assertSame($route, $this->_object->route('http://www.example.com:9999/one/two?some=param'));
    }

    /**
     * @covers Core_Router::route
     */
    public function testRoutingWillReturnTheRouteMatchingPath()
    {
        $route = $route = $this->_mockRoute('name', array('match'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with($this->equalTo('/one/two'))
            ->will($this->returnValue(true));
        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->addRoute($route);

        $this->assertSame($route, $this->_object->route('/one/two'));
    }

    /**
     * @covers Core_Router::route
     */
    public function testRoutingWillSetMatchingRouteAsCurrentRoute()
    {
        $route = $route = $this->_mockRoute('name', array('match'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with($this->equalTo('/one/two'))
            // "I DO match this path!"
            ->will($this->returnValue(true));

        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->addRoute($route)
            ->route('/one/two');

        $this->assertSame($route, $this->_object->getCurrentRoute());
    }

    /**
     * @covers Core_Router::route
     */
    public function testRoutingWillSetSetValuesFromMatchingRouteAsRequestParams()
    {
        $route = $route = $this->_mockRoute('name', array('match', 'getValues'));
        $route
            ->expects($this->once())
            ->method('match')
            ->with($this->equalTo('/one/two'))
            ->will($this->returnValue(true));

        // route has values to share!
        $route
            ->expects($this->once())
            ->method('getValues')
            ->will($this->returnValue(array('a' => 1, 'b' => 2)));

        // request object router will try to set the params to
        $request = $this->getMock('Core_Request', array('setParam'));

        // first route value
        $request
            ->expects($this->at(0))
            ->method('setParam')
            ->with($this->equalTo('a'), $this->equalTo(1));

        // second route value
        $request
            ->expects($this->at(1))
            ->method('setParam')
            ->with($this->equalTo('b'), $this->equalTo(2));

        $this->_object
            ->setRequest($request)
            ->addRoute($route)
            ->route('/one/two');
    }

    /**
     * @covers Core_Router::setBase
     * @covers Core_Router::getBase
     */
    public function testSetAndGetBaseRoute()
    {
        $this->_object->setBase('/some/base/route');
        $this->assertEquals('/some/base/route', $this->_object->getBase());
    }

    /**
     * @covers Core_Router::getBase
     */
    public function testWillRightTrimSeparatorFromBase()
    {
        $this->_object->setBase('/some/base/route/');
        $this->assertEquals('/some/base/route', $this->_object->getBase());
    }

    /**
     * @covers Core_Router::path
     * @covers Core_Router::<protected>
     */
    public function testGeneratingAnPathWithoutParamsUsesTheCurrentRoute()
    {
        $route = $this->getMockForAbstractClass('Core_Router_Route_Abstract');
        $route
            ->expects($this->once())
            ->method('path')
            ->with($this->equalTo('/'), $this->equalTo(null))
            ->will($this->returnValue('/foo'));

        $this->_object->setCurrentRoute($route);

        $this->assertEquals('/foo', $this->_object->path());
    }

    /**
     * @covers Core_Router::path
     * @covers Core_Router::<protected>
     */
    public function testGeneratingAnUrlWithoutARouteSpecifiedAndNoCurrentRouteThrowsAnInvalidArgumentException()
    {
        $this->setExpectedException('InvalidArgumentException');
        $this->_object->path();
    }

    /**
     * @covers Core_Router::path
     * @covers Core_Router::<protected>
     */
    public function testCanGenerateAnUrlWithParams()
    {
        $route = $this->getMockForAbstractClass('Core_Router_Route_Abstract');
        $route
            ->expects($this->once())
            ->method('path')
            ->with($this->equalTo('/'), $this->equalTo(array('a' => 'b')))
            ->will($this->returnValue('/foo'));

        $this->_object->setCurrentRoute($route);

        $this->assertEquals('/foo', $this->_object->path(array('a' => 'b')));
    }

    /**
     * @covers Core_Router::path
     * @covers Core_Router::<protected>
     */
    public function testSettingRouteNameWillUseThatRouteToGeneratePath()
    {
        $route = $this->_mockRoute('name', array('url'));
        $route
            ->expects($this->once())
            ->method('getName')
            ->will($this->returnValue('name'));
        $route
            ->expects($this->once())
            ->method('path')
            ->with($this->equalTo('/'), $this->equalTo(array('a' => 'b')))
            ->will($this->returnValue('/foo'));
        $currentRoute = $this->_mockRoute('foo');
        $this->_object
            ->addRoutes(array($route, $currentRoute))
            ->setCurrentRoute($currentRoute);

        $this->assertEquals('/foo', $this->_object->path(array('a' => 'b'), 'name'));
    }

    /**
     * @covers Core_Router::path
     * @covers Core_Router::<protected>
     */
    public function testCanNameTheCurrentRouteWhenGeneratingPath()
    {
        $route = $this->_mockRoute('name', array('url'));
        $route
            ->expects($this->once())
            ->method('path')
            ->with($this->equalTo('/'), $this->equalTo(array('a' => 'b')))
            ->will($this->returnValue('/foo'));

        $this->_object
            ->addRoute($route)
            ->setCurrentRoute($route);

        $this->assertEquals('/foo', $this->_object->path(array('a' => 'b'), 'name'));
    }

    /**
     * @covers Core_Router::path
     * @covers Core_Router::<protected>
     */
    public function testUrlWillBePrefixedWithTheBase()
    {
        $route = $this->_mockRoute('name', array('path'));
        $route
            ->expects($this->once())
            ->method('path')
            ->will($this->returnValue('/some/base/foo'));
        $this->_object
            ->setBase('/some/base/')
            ->addRoute($route)
            ->setCurrentRoute($route);

        $this->assertEquals('/some/base/foo', $this->_object->path());
    }

    /**
     * @covers Core_Router::path
     */
    public function testIfBuildingAPathForCurrentRouteWillUseItsValuesAsDefaults()
    {
        $route = $this->_mockRoute(null, array('getValues', 'path'));
        $route
            ->expects($this->once())
            ->method('getValues')
            ->will($this->returnValue(array('value' => 'original', 'foo' => 'bar')));
        $route
            ->expects($this->once())
            ->method('path')
            ->with($this->equalTo('/'), $this->equalTo(array('value' => 'overriden', 'foo' => 'bar', 'bla' => 23)));
        $this->_object
            ->setSeparator('/')
            ->setCurrentRoute($route);
        
        $this->_object->path(array('value' => 'overriden', 'bla' => 23));
    }

    /**
     * @covers Core_Router::url
     * @covers Core_Router::<protected>
     */
    public function testCanAssembleUrl()
    {
        $route = $this->_mockRoute('name', array('path'));
        $route
            ->setHostname('http://example.com')

            ->expects($this->once())
            ->method('path')
            ->will($this->returnValue('/foo'));
        $this->_object
            ->addRoute($route)
            ->setCurrentRoute($route);

        $this->assertEquals('http://example.com/foo', $this->_object->url());
    }
    
    /**
     * @covers Core_Router::url
     */
    public function testIfBuildingAUrlForCurrentRouteWillUseItsValuesAsDefaults()
    {
        $route = $this->_mockRoute(null, array('getValues', 'url'));
        $route
            ->expects($this->once())
            ->method('getValues')
            ->will($this->returnValue(array('value' => 'original', 'foo' => 'bar')));
        $route
            ->expects($this->once())
            ->method('url')
            ->with($this->equalTo('/'), $this->equalTo(array('value' => 'overriden', 'foo' => 'bar', 'bla' => 23)));
        $this->_object
            ->setSeparator('/')
            ->setCurrentRoute($route);
        
        $this->_object->url(array('value' => 'overriden', 'bla' => 23));
    }

    /**
     * @covers Core_Router::setDefaultHostname
     * @covers Core_Router::getDefaultHostname
     */
    public function testCanSetAndGetDefaultHostname()
    {
        $this->_object->setDefaultHostname('http://example.com');
        $this->assertEquals('http://example.com', $this->_object->getDefaultHostname());
    }

    /**
     * @covers Core_Router::getDefaultHostname
     */
    public function testFetchingTheHostnameWithoutOneSetThrowsARuntimeException()
    {
        $this->setExpectedException('RuntimeException');

        $this->_object->getDefaultHostname();
    }
    
    /**
     * @covers Core_Router::setDefaults
     * @covers Core_Router::setDefault
     * @covers Core_Router::getDefaults
     */
    public function testCanSetAndGetDefaults()
    {
        $this->_object->setDefault('bla', 99);
        $this->_object->setDefaults(array('abc' => 23, 'bla' => 1));

        $this->assertEquals(array('bla' => 1, 'abc' => 23), $this->_object->getDefaults());
    }

    /**
     * @covers Core_Router::setDefaults
     */
    public function testSettingDefaultsAddsToCurrentDefaults()
    {
        $this->_object->setDefaults(array('foo' => 'bar'));
        $this->_object->setDefaults(array('bar' => 'baz'));

        $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $this->_object->getDefaults());
    }

    /**
     * @covers Core_Router::getDefault
     */
    public function testCanGetDefaultByName()
    {
        $this->_object->setDefaults(array('foo' => 'bar'));

        $this->assertEquals('bar', $this->_object->getDefault('foo'));
    }
    
    /**
     * @covers Core_Router::getDefault
     */
    public function testFetchingAnUnknownDefaultReturnsNull()
    {
        $this->assertNull($this->_object->getDefault('unknown'));
    }

    protected function _mockContainer($name, $value)
    {
        // mock the container
        $container = $this->getMock('Core_Yadif_Container', array('getComponent'));
        $container
            ->expects($this->once())
            ->method('getComponent')
            ->with($this->equalTo($name))
            ->will($this->returnValue($value));

        return $container;
    }

    protected function _mockRoute($name = null, $methods = array())
    {
        // mock route
        $route = $this->getMockForAbstractClass(
            'Core_Router_Route_Abstract', array(), '', true, true, true, array_merge(array('getName'), $methods)
        );
        if (null !== $name) {
            $route
                ->expects($this->once())
                ->method('getName')
                ->will($this->returnValue($name));
        }
        return $route;
    }

    protected function _mockPluginLoader($name, $plugin, $type = 'Type', $options = array('opt' => 'ions'))
    {
        $pluginLoader = $this->getMock('Core_Loader_PluginLoader', array('initializeRouterRoutePlugin'));
        $pluginLoader
            ->expects($this->once())
            ->method('initializeRouterRoutePlugin')
            ->with($this->equalTo($type), $this->equalTo($name), $this->equalTo($options))
            ->will($this->returnValue($plugin));

        return $pluginLoader;
    }
}