<?php

/**
 * @link https://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license https://www.yiiframework.com/license/
 */

namespace yiiunit\framework\web;

use yii\web\Application;
use yii\caching\ArrayCache;
use yii\web\Request;
use yii\web\UrlManager;
use yiiunit\TestCase;

/**
 * This class implements the tests for URL parsing with "pretty" url format.
 *
 * See [[UrlManagerTest]] for tests with "default" URL format.
 * See [[UrlManagerCreateUrlTest]] for url creation with "pretty" URL format.
 *
 * Behavior of UrlManager::parseRequest() for the "pretty" URL format varies among the following options:
 *  - strict parsing = true / false
 *  - rules format
 *    - key => value
 *    - array config
 *
 * The following features are tested:
 *  - named parameters
 *    - as query params
 *    - as controller/actions '<controller:(post|comment)>/<id:\d+>/<action:(update|delete)>' => '<controller>/<action>',
 *  - Rules with Server Names
 *    - with protocol
 *    - without protocol i.e protocol relative, see https://github.com/yiisoft/yii2/pull/12697
 *    - with parameters
 *  - with suffix
 *  - with default values
 *  - with HTTP methods
 *
 *  - Adding rules dynamically
 *  - Test custom rules that only implement the interface
 *
 * NOTE: if a test is added here, you probably also need to add one in UrlManagerCreateUrlTest.
 *
 * @group web
 */
class UrlManagerParseUrlTest extends TestCase
{
    protected function getUrlManager(array $config = []): UrlManager
    {
        // in this test class, all tests have enablePrettyUrl enabled.
        $config['enablePrettyUrl'] = true;
        // normalizer is tested in UrlNormalizerTest
        $config['normalizer'] = false;

        return new UrlManager(array_merge([
            'cache' => null,
        ], $config));
    }

    protected function getRequest($pathInfo, $hostInfo = 'http://www.example.com', $method = 'GET', $config = []): Request
    {
        $config['pathInfo'] = $pathInfo;
        $config['hostInfo'] = $hostInfo;
        $_POST['_method'] = $method;
        return new Request($config);
    }

    protected function tearDown(): void
    {
        unset($_POST['_method']);
        parent::tearDown();
    }

    public function testWithoutRules(): void
    {
        $manager = $this->getUrlManager();

        // empty pathinfo
        $result = $manager->parseRequest($this->getRequest(''));
        $this->assertEquals(['', []], $result);
        // normal pathinfo
        $result = $manager->parseRequest($this->getRequest('site/index'));
        $this->assertEquals(['site/index', []], $result);
        // pathinfo with module
        $result = $manager->parseRequest($this->getRequest('module/site/index'));
        $this->assertEquals(['module/site/index', []], $result);
        // pathinfo with trailing slashes
        $result = $manager->parseRequest($this->getRequest('module/site/index/'));
        $this->assertEquals(['module/site/index/', []], $result);
    }

    public function testWithoutRulesStrict(): void
    {
        $manager = $this->getUrlManager();
        $manager->enableStrictParsing = true;

        // empty pathinfo
        $this->assertFalse($manager->parseRequest($this->getRequest('')));
        // normal pathinfo
        $this->assertFalse($manager->parseRequest($this->getRequest('site/index')));
        // pathinfo with module
        $this->assertFalse($manager->parseRequest($this->getRequest('module/site/index')));
        // pathinfo with trailing slashes
        $this->assertFalse($manager->parseRequest($this->getRequest('module/site/index/')));
    }

    public static function suffixProvider(): array
    {
        return [
            ['.html'],
            ['/'],
        ];
    }

    /**
     * @dataProvider suffixProvider
     *
     * @param string $suffix The URL suffix.
     */
    public function testWithoutRulesWithSuffix(string $suffix): void
    {
        $manager = $this->getUrlManager(['suffix' => $suffix]);

        // empty pathinfo
        $result = $manager->parseRequest($this->getRequest(''));
        $this->assertEquals(['', []], $result);
        // normal pathinfo
        $result = $manager->parseRequest($this->getRequest('site/index'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("site/index$suffix"));
        $this->assertEquals(['site/index', []], $result);
        // pathinfo with module
        $result = $manager->parseRequest($this->getRequest('module/site/index'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("module/site/index$suffix"));
        $this->assertEquals(['module/site/index', []], $result);
        // pathinfo with trailing slashes
        if ($suffix !== '/') {
            $result = $manager->parseRequest($this->getRequest('module/site/index/'));
            $this->assertFalse($result);
        }
        $result = $manager->parseRequest($this->getRequest("module/site/index/$suffix"));
        $this->assertEquals(['module/site/index/', []], $result);
    }

    public function testSimpleRules(): void
    {
        $config = [
            'rules' => [
                'post/<id:\d+>' => 'post/view',
                'posts' => 'post/index',
                'book/<id:\d+>/<title>' => 'book/view',
            ],
        ];
        $manager = $this->getUrlManager($config);

        // matching pathinfo
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample'));
        $this->assertEquals(['book/view', ['id' => '123', 'title' => 'this+is+sample']], $result);
        // trailing slash is significant, no match
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample/'));
        $this->assertEquals(['book/123/this+is+sample/', []], $result);
        // empty pathinfo
        $result = $manager->parseRequest($this->getRequest(''));
        $this->assertEquals(['', []], $result);
        // normal pathinfo
        $result = $manager->parseRequest($this->getRequest('site/index'));
        $this->assertEquals(['site/index', []], $result);
        // pathinfo with module
        $result = $manager->parseRequest($this->getRequest('module/site/index'));
        $this->assertEquals(['module/site/index', []], $result);
    }

    public function testSimpleRulesStrict(): void
    {
        $config = [
            'rules' => [
                'post/<id:\d+>' => 'post/view',
                'posts' => 'post/index',
                'book/<id:\d+>/<title>' => 'book/view',
            ],
        ];
        $manager = $this->getUrlManager($config);
        $manager->enableStrictParsing = true;

        // matching pathinfo
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample'));
        $this->assertEquals(['book/view', ['id' => '123', 'title' => 'this+is+sample']], $result);
        // trailing slash is significant, no match
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample/'));
        $this->assertFalse($result);
        // empty pathinfo
        $result = $manager->parseRequest($this->getRequest(''));
        $this->assertFalse($result);
        // normal pathinfo
        $result = $manager->parseRequest($this->getRequest('site/index'));
        $this->assertFalse($result);
        // pathinfo with module
        $result = $manager->parseRequest($this->getRequest('module/site/index'));
        $this->assertFalse($result);
    }

    /**
     * @dataProvider suffixProvider
     *
     * @param string $suffix The URL suffix.
     */
    public function testSimpleRulesWithSuffix(string $suffix): void
    {
        $config = [
            'rules' => [
                'post/<id:\d+>' => 'post/view',
                'posts' => 'post/index',
                'book/<id:\d+>/<title>' => 'book/view',
            ],
            'suffix' => $suffix,
        ];
        $manager = $this->getUrlManager($config);

        // matching pathinfo
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("book/123/this+is+sample$suffix"));
        $this->assertEquals(['book/view', ['id' => '123', 'title' => 'this+is+sample']], $result);
        // trailing slash is significant, no match
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample/'));
        if ($suffix === '/') {
            $this->assertEquals(['book/view', ['id' => '123', 'title' => 'this+is+sample']], $result);
        } else {
            $this->assertFalse($result);
        }
        $result = $manager->parseRequest($this->getRequest("book/123/this+is+sample/$suffix"));
        $this->assertEquals(['book/123/this+is+sample/', []], $result);
        // empty pathinfo
        $result = $manager->parseRequest($this->getRequest(''));
        $this->assertEquals(['', []], $result);
        // normal pathinfo
        $result = $manager->parseRequest($this->getRequest('site/index'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("site/index$suffix"));
        $this->assertEquals(['site/index', []], $result);
        // pathinfo with module
        $result = $manager->parseRequest($this->getRequest('module/site/index'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("module/site/index$suffix"));
        $this->assertEquals(['module/site/index', []], $result);
    }

    /**
     * @dataProvider suffixProvider
     *
     * @param string $suffix The URL suffix.
     */
    public function testSimpleRulesWithSuffixStrict(string $suffix): void
    {
        $config = [
            'rules' => [
                'post/<id:\d+>' => 'post/view',
                'posts' => 'post/index',
                'book/<id:\d+>/<title>' => 'book/view',
            ],
            'suffix' => $suffix,
        ];
        $manager = $this->getUrlManager($config);
        $manager->enableStrictParsing = true;

        // matching pathinfo
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("book/123/this+is+sample$suffix"));
        $this->assertEquals(['book/view', ['id' => '123', 'title' => 'this+is+sample']], $result);
        // trailing slash is significant, no match
        $result = $manager->parseRequest($this->getRequest('book/123/this+is+sample/'));
        if ($suffix === '/') {
            $this->assertEquals(['book/view', ['id' => '123', 'title' => 'this+is+sample']], $result);
        } else {
            $this->assertFalse($result);
        }
        $result = $manager->parseRequest($this->getRequest("book/123/this+is+sample/$suffix"));
        $this->assertFalse($result);
        // empty pathinfo
        $result = $manager->parseRequest($this->getRequest(''));
        $this->assertFalse($result);
        // normal pathinfo
        $result = $manager->parseRequest($this->getRequest('site/index'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("site/index$suffix"));
        $this->assertFalse($result);
        // pathinfo with module
        $result = $manager->parseRequest($this->getRequest('module/site/index'));
        $this->assertFalse($result);
        $result = $manager->parseRequest($this->getRequest("module/site/index$suffix"));
        $this->assertFalse($result);
    }

    // TODO implement with hostinfo
    public function testParseRESTRequest(): void
    {
        $request = new Request();

        // pretty URL rules
        $manager = new UrlManager([
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'cache' => null,
            'rules' => [
                'PUT,POST post/<id>/<title>' => 'post/create',
                'DELETE post/<id>' => 'post/delete',
                'post/<id>/<title>' => 'post/view',
                'POST/GET' => 'post/get',
            ],
        ]);
        // matching pathinfo GET request
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $request->pathInfo = 'post/123/this+is+sample';
        $result = $manager->parseRequest($request);
        $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result);
        // matching pathinfo PUT/POST request
        $_SERVER['REQUEST_METHOD'] = 'PUT';
        $request->pathInfo = 'post/123/this+is+sample';
        $result = $manager->parseRequest($request);
        $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result);
        $_SERVER['REQUEST_METHOD'] = 'POST';
        $request->pathInfo = 'post/123/this+is+sample';
        $result = $manager->parseRequest($request);
        $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result);

        // no wrong matching
        $_SERVER['REQUEST_METHOD'] = 'POST';
        $request->pathInfo = 'POST/GET';
        $result = $manager->parseRequest($request);
        $this->assertEquals(['post/get', []], $result);

        // createUrl should ignore REST rules
        $this->mockApplication([
            'components' => [
                'request' => [
                    'hostInfo' => 'http://localhost/',
                    'baseUrl' => '/app',
                ],
            ],
        ], Application::class);
        $this->assertEquals('/app/post/123', $manager->createUrl(['post/delete', 'id' => 123]));
        $this->destroyApplication();

        unset($_SERVER['REQUEST_METHOD']);
    }

    public function testAppendRules(): void
    {
        $manager = $this->getUrlManager(['rules' => ['post/<id:\d+>' => 'post/view']]);

        $this->assertCount(1, $manager->rules);
        $firstRule = $manager->rules[0];
        $this->assertInstanceOf('yii\web\UrlRuleInterface', $firstRule);

        $manager->addRules([
            'posts' => 'post/index',
            'book/<id:\d+>/<title>' => 'book/view',
        ]);
        $this->assertCount(3, $manager->rules);
        $this->assertSame($firstRule, $manager->rules[0]);
    }

    public function testPrependRules(): void
    {
        $manager = $this->getUrlManager(['rules' => ['post/<id:\d+>' => 'post/view']]);

        $this->assertCount(1, $manager->rules);
        $firstRule = $manager->rules[0];
        $this->assertInstanceOf('yii\web\UrlRuleInterface', $firstRule);

        $manager->addRules([
            'posts' => 'post/index',
            'book/<id:\d+>/<title>' => 'book/view',
        ], false);
        $this->assertCount(3, $manager->rules);
        $this->assertNotSame($firstRule, $manager->rules[0]);
        $this->assertSame($firstRule, $manager->rules[2]);
    }

    public function testRulesCache(): void
    {
        $arrayCache = new ArrayCache();

        $manager = $this->getUrlManager([
            'rules' => ['post/<id:\d+>' => 'post/view'],
            'cache' => $arrayCache,
        ]);

        $this->assertCount(1, $manager->rules);
        $firstRule = $manager->rules[0];
        $this->assertInstanceOf('yii\web\UrlRuleInterface', $firstRule);
        $this->assertCount(
            1,
            $this->getInaccessibleProperty($arrayCache, '_cache'),
            'Cache contains the only one record that represents initial built rules'
        );

        $manager->addRules(['posts' => 'post/index']);
        $manager->addRules([
            'book/<id:\d+>/<title>' => 'book/view',
            'book/<id:\d+>/<author>' => 'book/view'
        ]);

        $this->assertCount(4, $manager->rules);
        $this->assertSame($firstRule, $manager->rules[0]);
        $this->assertCount(
            3,
            $this->getInaccessibleProperty($arrayCache, '_cache'),
            'The addRules() method was called twice, adding 3 new rules to the UrlManager, but we have only ' .
            'two additional caches: one for each addRules() method call.'
        );
    }

    public function testRulesCacheIsUsed(): void
    {
        $arrayCache = $this->createPartialMock(
            ArrayCache::class,
            [
                'get',
                'set',
            ],
        );

        $manager = $this->getUrlManager([
            'rules' => ['post/<id:\d+>' => 'post/view'],
            'cache' => $arrayCache,
        ]);

        $savedRules = $manager->rules;
        // save rules to "cache" and make sure it is reused
        $arrayCache->expects($this->exactly(2))->method('get')->willReturn($savedRules);
        $arrayCache->expects($this->never())->method('set');

        for ($i = 0; $i < 2; $i++) {
            $this->getUrlManager([
                'rules' => ['post/<id:\d+>' => 'post/view'],
                'cache' => $arrayCache,
            ]);
        }
    }

    /**
     * Test a scenario where catch-all rule is used at the end for a CMS but module names should use the module actions and controllers.
     */
    public function testModuleRoute(): void
    {
        $modules = 'user|my-admin';

        $manager = $this->getUrlManager([
            'rules' => [
                "<module:$modules>" => '<module>',
                "<module:$modules>/<controller>" => '<module>/<controller>',
                "<module:$modules>/<controller>/<action>" => '<module>/<controller>/<action>',
                '<url:[a-zA-Z0-9-/]+>' => 'site/index',
            ],
        ]);

        $result = $manager->parseRequest($this->getRequest('user'));
        $this->assertEquals(['user', []], $result);
        $result = $manager->parseRequest($this->getRequest('user/somecontroller'));
        $this->assertEquals(['user/somecontroller', []], $result);
        $result = $manager->parseRequest($this->getRequest('user/somecontroller/someaction'));
        $this->assertEquals(['user/somecontroller/someaction', []], $result);

        $result = $manager->parseRequest($this->getRequest('users/somecontroller/someaction'));
        $this->assertEquals(['site/index', ['url' => 'users/somecontroller/someaction']], $result);
    }
}
