<?php

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

namespace yiiunit\framework\filters;

use Yii;
use yii\filters\RateLimiter;
use yii\web\Request;
use yii\web\Response;
use yii\web\User;
use yiiunit\framework\filters\stubs\ExposedLogger;
use yiiunit\framework\filters\stubs\RateLimit;
use yiiunit\TestCase;

/**
 *  @group filters
 */
class RateLimiterTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        Yii::setLogger(new ExposedLogger());

        $this->mockWebApplication();
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        Yii::setLogger(null);
    }

    public function testInitFilledRequest(): void
    {
        $rateLimiter = new RateLimiter(['request' => 'Request']);

        $this->assertEquals('Request', $rateLimiter->request);
    }

    public function testInitNotFilledRequest(): void
    {
        $rateLimiter = new RateLimiter();

        $this->assertInstanceOf(Request::class, $rateLimiter->request);
    }

    public function testInitFilledResponse(): void
    {
        $rateLimiter = new RateLimiter(['response' => 'Response']);

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

    public function testInitNotFilledResponse(): void
    {
        $rateLimiter = new RateLimiter();

        $this->assertInstanceOf(Response::class, $rateLimiter->response);
    }

    public function testBeforeActionUserInstanceOfRateLimitInterface(): void
    {
        $rateLimiter = new RateLimiter();
        $rateLimit = new RateLimit();
        $rateLimit->setAllowance([1, time()])
            ->setRateLimit([1, 1]);
        $rateLimiter->user = $rateLimit;

        $result = $rateLimiter->beforeAction('test');

        $this->assertContains('Check rate limit', Yii::getLogger()->messages);
        $this->assertTrue($result);
    }

    public function testBeforeActionUserNotInstanceOfRateLimitInterface(): void
    {
        $rateLimiter = new RateLimiter(['user' => 'User']);

        $result = $rateLimiter->beforeAction('test');

        $this->assertContains(
            'Rate limit skipped: "user" does not implement RateLimitInterface.',
            Yii::getLogger()->messages
        );
        $this->assertTrue($result);
    }

    public function testBeforeActionEmptyUser(): void
    {
        $user = new User(['identityClass' => RateLimit::class]);
        Yii::$app->set('user', $user);
        $rateLimiter = new RateLimiter();

        $result = $rateLimiter->beforeAction('test');

        $this->assertContains('Rate limit skipped: user not logged in.', Yii::getLogger()->messages);
        $this->assertTrue($result);
    }

    public function testCheckRateLimitTooManyRequests(): void
    {
        $rateLimit = new RateLimit();
        $rateLimit
            ->setRateLimit([1, 1])
            ->setAllowance([1, time() + 2]);
        $rateLimiter = new RateLimiter();

        $this->expectException('yii\web\TooManyRequestsHttpException');
        $rateLimiter->checkRateLimit($rateLimit, Yii::$app->request, Yii::$app->response, 'testAction');
    }

    public function testCheckRateaddRateLimitHeaders(): void
    {
        $rateLimit = new RateLimit();
        $rateLimit
            ->setRateLimit([2, 10])
            ->setAllowance([2, time()]);

        $rateLimiter = new RateLimiter();
        $response = Yii::$app->response;
        $rateLimiter->checkRateLimit($rateLimit, Yii::$app->request, $response, 'testAction');
        $headers = $response->getHeaders();
        $this->assertEquals(2, $headers->get('X-Rate-Limit-Limit'));
        $this->assertEquals(1, $headers->get('X-Rate-Limit-Remaining'));
        $this->assertEquals(5, $headers->get('X-Rate-Limit-Reset'));
    }

    public function testAddRateLimitHeadersDisabledRateLimitHeaders(): void
    {
        $rateLimiter = new RateLimiter();
        $rateLimiter->enableRateLimitHeaders = false;
        $response = Yii::$app->response;

        $rateLimiter->addRateLimitHeaders($response, 1, 0, 0);
        $this->assertCount(0, $response->getHeaders());
    }

    public function testAddRateLimitHeadersEnabledRateLimitHeaders(): void
    {
        $rateLimiter = new RateLimiter();
        $rateLimiter->enableRateLimitHeaders = true;
        $response = Yii::$app->response;

        $rateLimiter->addRateLimitHeaders($response, 1, 0, 0);
        $this->assertCount(3, $response->getHeaders());
    }

    /**
     * @see https://github.com/yiisoft/yii2/issues/18236
     */
    public function testUserWithClosureFunction(): void
    {
        $rateLimiter = new RateLimiter();
        $rateLimiter->user = function ($action) {
            return new User(['identityClass' => RateLimit::class]);
        };
        $rateLimiter->beforeAction('test');

        // testing the evaluation of user closure, which in this case returns not the expect object and therefore
        // the log message "does not implement RateLimitInterface" is expected.
        $this->assertInstanceOf(User::class, $rateLimiter->user);
        $this->assertContains(
            'Rate limit skipped: "user" does not implement RateLimitInterface.',
            Yii::getLogger()->messages
        );
    }
}
