<?php

/*
 * This file is part of the Symfony CMF package.
 *
 * (c) 2011-2015 Symfony CMF
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Cmf\Component\Routing\Tests\Routing;

use Symfony\Cmf\Component\Routing\VersatileGeneratorInterface;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Cmf\Component\Routing\ChainRouter;
use Symfony\Cmf\Component\Routing\Test\CmfUnitTestCase;
use Symfony\Component\Routing\RouterInterface;

class ChainRouterTest extends CmfUnitTestCase
{
    /**
     * @var ChainRouter
     */
    private $router;
    /**
     * @var RequestContext|\PHPUnit_Framework_MockObject_MockObject
     */
    private $context;

    public function setUp()
    {
        $this->router = new ChainRouter($this->getMock('Psr\Log\LoggerInterface'));
        $this->context = $this->getMock('Symfony\Component\Routing\RequestContext');
    }

    public function testPriority()
    {
        $this->assertEquals(array(), $this->router->all());

        list($low, $high) = $this->createRouterMocks();

        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->assertEquals(array(
            $high,
            $low,
        ), $this->router->all());
    }

    public function testHasRouters()
    {
        $this->assertEquals(array(), $this->router->all());
        $this->assertFalse($this->router->hasRouters());

        list($low, $high) = $this->createRouterMocks();

        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->assertTrue($this->router->hasRouters());
    }

    /**
     * Routers are supposed to be sorted only once.
     * This test will check that by trying to get all routers several times.
     *
     * @covers \Symfony\Cmf\Component\Routing\ChainRouter::sortRouters
     * @covers \Symfony\Cmf\Component\Routing\ChainRouter::all
     */
    public function testSortRouters()
    {
        list($low, $medium, $high) = $this->createRouterMocks();
        // We're using a mock here and not $this->router because we need to ensure that the sorting operation is done only once.
        /** @var $router ChainRouter|\PHPUnit_Framework_MockObject_MockObject */
        $router = $this->buildMock('Symfony\Cmf\Component\Routing\ChainRouter', array('sortRouters'));
        $router
            ->expects($this->once())
            ->method('sortRouters')
            ->will(
                $this->returnValue(
                    array($high, $medium, $low)
                )
            )
        ;

        $router->add($low, 10);
        $router->add($medium, 50);
        $router->add($high, 100);
        $expectedSortedRouters = array($high, $medium, $low);
        // Let's get all routers 5 times, we should only sort once.
        for ($i = 0; $i < 5; ++$i) {
            $this->assertSame($expectedSortedRouters, $router->all());
        }
    }

    /**
     * This test ensures that if a router is being added on the fly, the sorting is reset.
     *
     * @covers \Symfony\Cmf\Component\Routing\ChainRouter::sortRouters
     * @covers \Symfony\Cmf\Component\Routing\ChainRouter::all
     * @covers \Symfony\Cmf\Component\Routing\ChainRouter::add
     */
    public function testReSortRouters()
    {
        list($low, $medium, $high) = $this->createRouterMocks();
        $highest = clone $high;
        // We're using a mock here and not $this->router because we need to ensure that the sorting operation is done only once.
        /** @var $router ChainRouter|\PHPUnit_Framework_MockObject_MockObject */
        $router = $this->buildMock('Symfony\Cmf\Component\Routing\ChainRouter', array('sortRouters'));
        $router
            ->expects($this->at(0))
            ->method('sortRouters')
            ->will(
                $this->returnValue(
                    array($high, $medium, $low)
                )
            )
        ;
        // The second time sortRouters() is called, we're supposed to get the newly added router ($highest)
        $router
            ->expects($this->at(1))
            ->method('sortRouters')
            ->will(
                $this->returnValue(
                    array($highest, $high, $medium, $low)
                )
            )
        ;

        $router->add($low, 10);
        $router->add($medium, 50);
        $router->add($high, 100);
        $this->assertSame(array($high, $medium, $low), $router->all());

        // Now adding another router on the fly, sorting must have been reset
        $router->add($highest, 101);
        $this->assertSame(array($highest, $high, $medium, $low), $router->all());
    }

    /**
     * context must be propagated to chained routers and be stored locally.
     */
    public function testContext()
    {
        list($low, $high) = $this->createRouterMocks();

        $low
            ->expects($this->once())
            ->method('setContext')
            ->with($this->context)
        ;

        $high
            ->expects($this->once())
            ->method('setContext')
            ->with($this->context)
        ;

        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->setContext($this->context);
        $this->assertSame($this->context, $this->router->getContext());
    }

    /**
     * context must be propagated also when routers are added after context is set.
     */
    public function testContextOrder()
    {
        list($low, $high) = $this->createRouterMocks();

        $low
            ->expects($this->once())
            ->method('setContext')
            ->with($this->context)
        ;

        $high
            ->expects($this->once())
            ->method('setContext')
            ->with($this->context)
        ;

        $this->router->setContext($this->context);

        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->all();

        $this->assertSame($this->context, $this->router->getContext());
    }

    /**
     * The first usable match is used, no further routers are queried once a match is found.
     */
    public function testMatch()
    {
        $url = '/test';
        list($lower, $low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\ResourceNotFoundException()))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->returnValue(array('test')))
        ;
        $lower
            ->expects($this->never())
            ->method('match');
        $this->router->add($lower, 5);
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $result = $this->router->match('/test');
        $this->assertEquals(array('test'), $result);
    }

    /**
     * The first usable match is used, no further routers are queried once a match is found.
     */
    public function testMatchRequest()
    {
        $url = '/test';
        list($lower, $low, $high) = $this->createRouterMocks();

        $highest = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\RequestMatcher');

        $request = Request::create('/test');

        $highest
            ->expects($this->once())
            ->method('matchRequest')
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\ResourceNotFoundException()))
        ;
        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\ResourceNotFoundException()))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->returnValue(array('test')))
        ;
        $lower
            ->expects($this->never())
            ->method('match')
        ;

        $this->router->add($lower, 5);
        $this->router->add($low, 10);
        $this->router->add($high, 100);
        $this->router->add($highest, 200);

        $result = $this->router->matchRequest($request);
        $this->assertEquals(array('test'), $result);
    }

    /**
     * Call match on ChainRouter that has RequestMatcher in the chain.
     */
    public function testMatchWithRequestMatchers()
    {
        $url = '/test';

        list($low) = $this->createRouterMocks();

        $high = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\RequestMatcher');

        $high
            ->expects($this->once())
            ->method('matchRequest')
            ->with($this->callback(function (Request $r) use ($url) {
                return $r->getPathInfo() === $url;
            }))
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\ResourceNotFoundException()))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->returnValue(array('test')))
        ;

        $this->router->add($low, 10);
        $this->router->add($high, 20);

        $result = $this->router->match($url);
        $this->assertEquals(array('test'), $result);
    }

    public function provideBaseUrl()
    {
        return array(
            array(''),
            array('/web'),
        );
    }

    /**
     * Call match on ChainRouter that has RequestMatcher in the chain.
     *
     * @dataProvider provideBaseUrl
     */
    public function testMatchWithRequestMatchersAndContext($baseUrl)
    {
        $url = '//test';

        list($low) = $this->createRouterMocks();

        $high = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\RequestMatcher');

        $high
            ->expects($this->once())
            ->method('matchRequest')
            ->with($this->callback(function (Request $r) use ($url, $baseUrl) {
                return true === $r->isSecure()
                    && 'foobar.com' === $r->getHost()
                    && 4433 === $r->getPort()
                    && $baseUrl === $r->getBaseUrl()
                    && $url === $r->getPathInfo()
                ;
            }))
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\ResourceNotFoundException()))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->returnValue(array('test')))
        ;

        $this->router->add($low, 10);
        $this->router->add($high, 20);

        $requestContext = new RequestContext();
        $requestContext->setScheme('https');
        $requestContext->setHost('foobar.com');
        $requestContext->setHttpsPort(4433);
        $requestContext->setBaseUrl($baseUrl);
        $this->router->setContext($requestContext);

        $result = $this->router->match($url);
        $this->assertEquals(array('test'), $result);
    }

    /**
     * If there is a method not allowed but another router matches, that one is used.
     */
    public function testMatchAndNotAllowed()
    {
        $url = '/test';
        list($low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\MethodNotAllowedException(array())))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->returnValue(array('test')))
        ;
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $result = $this->router->match('/test');
        $this->assertEquals(array('test'), $result);
    }

    /**
     * If there is a method not allowed but another router matches, that one is used.
     */
    public function testMatchRequestAndNotAllowed()
    {
        $url = '/test';
        list($low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\MethodNotAllowedException(array())))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->returnValue(array('test')))
        ;
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $result = $this->router->matchRequest(Request::create('/test'));
        $this->assertEquals(array('test'), $result);
    }

    /**
     * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
     */
    public function testMatchNotFound()
    {
        $url = '/test';
        list($low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new ResourceNotFoundException()))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new ResourceNotFoundException()))
        ;
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->match('/test');
    }

    /**
     * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
     */
    public function testMatchRequestNotFound()
    {
        $url = '/test';
        list($low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new ResourceNotFoundException()))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new ResourceNotFoundException()))
        ;
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->matchRequest(Request::create('/test'));
    }

    /**
     * Call match on ChainRouter that has RequestMatcher in the chain.
     *
     * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
     * @expectedExceptionMessage None of the routers in the chain matched url '/test'
     */
    public function testMatchWithRequestMatchersNotFound()
    {
        $url = '/test';
        $request = Request::create('/test');

        $high = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\RequestMatcher');

        $high
            ->expects($this->once())
            ->method('matchRequest')
            ->with($request)
            ->will($this->throwException(new \Symfony\Component\Routing\Exception\ResourceNotFoundException()))
        ;

        $this->router->add($high, 20);

        $this->router->match($url);
    }

    /**
     * If any of the routers throws a not allowed exception and no other matches, we need to see this.
     *
     * @expectedException \Symfony\Component\Routing\Exception\MethodNotAllowedException
     */
    public function testMatchMethodNotAllowed()
    {
        $url = '/test';
        list($low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new MethodNotAllowedException(array())))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new ResourceNotFoundException()))
        ;
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->match('/test');
    }

    /**
     * If any of the routers throws a not allowed exception and no other matches, we need to see this.
     *
     * @expectedException \Symfony\Component\Routing\Exception\MethodNotAllowedException
     */
    public function testMatchRequestMethodNotAllowed()
    {
        $url = '/test';
        list($low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new MethodNotAllowedException(array())))
        ;
        $low
            ->expects($this->once())
            ->method('match')
            ->with($url)
            ->will($this->throwException(new ResourceNotFoundException()))
        ;
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->matchRequest(Request::create('/test'));
    }

    public function testGenerate()
    {
        $url = '/test';
        $name = 'test';
        $parameters = array('test' => 'value');
        list($lower, $low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('generate')
            ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH)
            ->will($this->throwException(new RouteNotFoundException()))
        ;
        $low
            ->expects($this->once())
            ->method('generate')
            ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH)
            ->will($this->returnValue($url))
        ;
        $lower
            ->expects($this->never())
            ->method('generate')
        ;

        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $result = $this->router->generate($name, $parameters);
        $this->assertEquals($url, $result);
    }

    /**
     * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException
     */
    public function testGenerateNotFound()
    {
        $name = 'test';
        $parameters = array('test' => 'value');
        list($low, $high) = $this->createRouterMocks();

        $high
            ->expects($this->once())
            ->method('generate')
            ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH)
            ->will($this->throwException(new RouteNotFoundException()))
        ;
        $low->expects($this->once())
            ->method('generate')
            ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH)
            ->will($this->throwException(new RouteNotFoundException()))
        ;
        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->generate($name, $parameters);
    }

    /**
     * Route is an object but no versatile generator around to do the debug message.
     *
     * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException
     */
    public function testGenerateObjectNotFound()
    {
        $name = new \stdClass();
        $parameters = array('test' => 'value');

        $defaultRouter = $this->getMock('Symfony\Component\Routing\RouterInterface');

        $defaultRouter
            ->expects($this->never())
            ->method('generate')
        ;

        $this->router->add($defaultRouter, 200);

        $this->router->generate($name, $parameters);
    }

    /**
     * A versatile router will generate the debug message.
     *
     * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException
     */
    public function testGenerateObjectNotFoundVersatile()
    {
        $name = new \stdClass();
        $parameters = array('test' => 'value');

        $chainedRouter = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\VersatileRouter');
        $chainedRouter
            ->expects($this->once())
            ->method('supports')
            ->will($this->returnValue(true))
        ;
        $chainedRouter->expects($this->once())
            ->method('generate')
            ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH)
            ->will($this->throwException(new RouteNotFoundException()))
        ;
        $chainedRouter->expects($this->once())
            ->method('getRouteDebugMessage')
            ->with($name, $parameters)
            ->will($this->returnValue('message'))
        ;

        $this->router->add($chainedRouter, 10);

        $this->router->generate($name, $parameters);
    }

    public function testGenerateObjectName()
    {
        $name = new \stdClass();
        $parameters = array('test' => 'value');

        $defaultRouter = $this->getMock('Symfony\Component\Routing\RouterInterface');
        $chainedRouter = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\VersatileRouter');

        $defaultRouter
            ->expects($this->never())
            ->method('generate')
        ;
        $chainedRouter
            ->expects($this->once())
            ->method('supports')
            ->will($this->returnValue(true))
        ;
        $chainedRouter
            ->expects($this->once())
            ->method('generate')
            ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH)
            ->will($this->returnValue($name))
        ;

        $this->router->add($defaultRouter, 200);
        $this->router->add($chainedRouter, 100);

        $result = $this->router->generate($name, $parameters);
        $this->assertEquals($name, $result);
    }

    public function testWarmup()
    {
        $dir = 'test_dir';
        list($low) = $this->createRouterMocks();

        $low
            ->expects($this->never())
            ->method('warmUp')
        ;
        $high = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\WarmableRouterMock');
        $high
            ->expects($this->once())
            ->method('warmUp')
            ->with($dir)
        ;

        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $this->router->warmUp($dir);
    }

    public function testRouteCollection()
    {
        list($low, $high) = $this->createRouterMocks();
        $lowcol = new RouteCollection();
        $lowcol->add('low', $this->buildMock('Symfony\Component\Routing\Route'));
        $highcol = new RouteCollection();
        $highcol->add('high', $this->buildMock('Symfony\Component\Routing\Route'));

        $low
            ->expects($this->once())
            ->method('getRouteCollection')
            ->will($this->returnValue($lowcol))
        ;
        $high
            ->expects($this->once())
            ->method('getRouteCollection')
            ->will($this->returnValue($highcol))
        ;

        $this->router->add($low, 10);
        $this->router->add($high, 100);

        $collection = $this->router->getRouteCollection();
        $this->assertInstanceOf('Symfony\Component\Routing\RouteCollection', $collection);

        $names = array();
        foreach ($collection->all() as $name => $route) {
            $this->assertInstanceOf('Symfony\Component\Routing\Route', $route);
            $names[] = $name;
        }
        $this->assertEquals(array('high', 'low'), $names);
    }

    /**
     * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException
     */
    public function testSupport()
    {
        $router = $this->getMock('Symfony\Cmf\Component\Routing\Tests\Routing\VersatileRouter');
        $router
            ->expects($this->once())
            ->method('supports')
            ->will($this->returnValue(false))
        ;

        $router
            ->expects($this->never())
            ->method('generate')
            ->will($this->returnValue(false))
        ;

        $this->router->add($router);

        $this->router->generate('foobar');
    }

    /**
     * @return RouterInterface[]|\PHPUnit_Framework_MockObject_MockObject[]
     */
    protected function createRouterMocks()
    {
        return array(
            $this->getMock('Symfony\Component\Routing\RouterInterface'),
            $this->getMock('Symfony\Component\Routing\RouterInterface'),
            $this->getMock('Symfony\Component\Routing\RouterInterface'),
        );
    }
}

abstract class WarmableRouterMock implements RouterInterface, WarmableInterface
{
}

abstract class RequestMatcher implements RouterInterface, RequestMatcherInterface
{
}

abstract class VersatileRouter implements VersatileGeneratorInterface, RequestMatcherInterface
{
}
