<?php

namespace Velis\Test\RateLimiter;

use Generator;
use Mockery;
use Velis\RateLimiter\LeakyBucketRateLimiterInterface;
use Velis\RateLimiter\RateLimitRouteWrapper;
use Velis\TestCase;

/**
 * @author Szymon Janaczek <szymon.janaczek@singu.com>
 */
final class RateLimitRouteWrapperTest extends TestCase
{
    /**
     * @dataProvider doesPathMatchDataProvider
     */
    public function testDoesPathMatch(string $currentUri, string $pathPattern, bool $expected): void
    {
        $rateLimitRouteWrapper = new RateLimitRouteWrapper(
            rateLimiter: $this->createStub(LeakyBucketRateLimiterInterface::class),
            pathsConfiguration: [],
        );

        $doesMatch = $rateLimitRouteWrapper->doesRouteMatch($currentUri, $pathPattern);

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

    /**
     * There are cases for REST api endpoint cases.
     * @return Generator<array{currentUri: string, pathPattern: string, expected: bool}>
     */
    public function doesPathMatchDataProvider(): Generator
    {
        yield 'exact match' => ['/users', '/users', true];
        yield 'regex match' => ['/users/1', '/users/[0-9]+', true];
        yield 'param regex match with continuation' => ['/users/1/items', '/users/[0-9]+/items', true];
        yield 'regex match with wildcard' => ['/users/1', '/users/*', true];
        yield 'regex match with param wildcard and continuation' => ['/users/1/items', '/users/*/items', true];
        yield 'regex match with wildcard and continuation' => ['/users/details', '/users/*', true];
        yield 'multi level nested uri' => ['/tickets/123/notes/321/author', '/tickets/*/notes/*/author', true];
        yield 'trailing slash in current uri' => ['/users/', '/users', true];
        yield 'trailing slash in path pattern' => ['/users', '/users/', true];
        yield 'no match with param' => ['/users/1', '/users', false];
        yield 'case insensitive match in current URI' => ['/USERS', '/users', true];
        yield 'case insensitive match in pattern' => ['/users', '/USERS', true];
        yield 'case insensitive match with regex' => ['/USERS/1', '/users/[0-9]+', true];
        yield 'case insensitive match with wildcard' => ['/USERS/1', '/users/*', true];
        yield 'too big nesting for one wildcard' => ['/tickets/users/nicknames/items', '/tickets/*/items', false];
        yield 'big nesting, multiple wildcard' => ['/tickets/users/nicknames/items', '/tickets/*/*/items', true];
        yield 'root path with super wildcard' => ['/api/tickets/observers/nicknames', '/api/**', true];
        yield 'root path with super wildcard and correct continuation' => [
            '/api/tickets/observers/nicknames/list', '/api/**/list', true,
        ];
        yield 'root path with super wildcard and wrong continuation' => [
            '/api/tickets/observers/nicknames/items', '/api/**/list', false,
            ];
    }

    /**
     * @dataProvider parseConfigStringDataProvider
     */
    public function testParseConfigString(string $configString, array $expected): void
    {
        $rateLimitRouteWrapper = new RateLimitRouteWrapper(
            rateLimiter: $this->createStub(LeakyBucketRateLimiterInterface::class),
            pathsConfiguration: [],
        );

        $parsedConfig = $rateLimitRouteWrapper->parseRouteConfig($configString);

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

    /**
     * @return Generator<array{
     *     configString: string, expected: array{max_attempts: int, decay_seconds: int, leak_rate: int}
     * }>
     */
    public function parseConfigStringDataProvider(): Generator
    {
        yield 'simple config' => [
            '10,60,1', ['max_attempts' => 10, 'decay_seconds' => 60, 'leak_rate' => 1],
        ];

        yield 'config with large values' => [
            '1000,3600,10', ['max_attempts' => 1000, 'decay_seconds' => 3600, 'leak_rate' => 10],
        ];

        yield 'config with small values' => [
            '1,1,1', ['max_attempts' => 1, 'decay_seconds' => 1, 'leak_rate' => 1],
        ];

        yield 'config with 0 max_attempts replaced by 1' => [
            '0,1,1', ['max_attempts' => 1, 'decay_seconds' => 1, 'leak_rate' => 1],
        ];

        yield 'config with 0 decay_seconds replaced by 1' => [
            '1,0,1', ['max_attempts' => 1, 'decay_seconds' => 1, 'leak_rate' => 1],
        ];

        yield 'config with 0 leak_rate replaced by 1' => [
            '1,1,0', ['max_attempts' => 1, 'decay_seconds' => 1, 'leak_rate' => 1],
        ];

        yield 'config with float max_attempts' => [
            '0.1,1,1', ['max_attempts' => 1, 'decay_seconds' => 1, 'leak_rate' => 1],
        ];

        yield 'config with float decay_seconds' => [
            '1,0.1,1', ['max_attempts' => 1, 'decay_seconds' => 1, 'leak_rate' => 1],
        ];

        yield 'config with float leak_rate' => [
            '1,1,0.1', ['max_attempts' => 1, 'decay_seconds' => 1, 'leak_rate' => 0.1],
        ];
    }

    /**
     * Check whether the hierarchy of paths is checked correctly.
     * @dataProvider checkUriDataProvider
     */
    public function testCheckUri(array $pathsConfiguration, string $currentUri, string $expectedPattern): void
    {
        // I know it's ugly, but it's the only way to test it.
        // Using Mockery::spy() with ->withArgs() returning true/false worked but marked test as incomplete/risky.
        // Using it with $this->assertTrue() inside instead of returning bool worked but marked test as risky.
        // Using Mockery::mock() didn't work - fatal errors occurred.
        // Maybe update to a newer version of PHP Unit would help.
        $passed = false;
        $rateLimiterMock = Mockery::spy(LeakyBucketRateLimiterInterface::class);
        $rateLimiterMock
            ->expects('attemptByIdentifier')
            ->withArgs(function ($rateLimit, $leakRate, $key) use ($expectedPattern, &$passed) {
                if ($expectedPattern === $key) {
                    $passed = true;
                }
            })
            ->once()
        ;

        $rateLimitRouteWrapper = new RateLimitRouteWrapper(
            rateLimiter: $rateLimiterMock,
            pathsConfiguration: $pathsConfiguration,
        );

        $rateLimitRouteWrapper->checkUri($currentUri);

        $this->assertTrue($passed);
    }

    /**
     * @return Generator<array{pathsConfiguration: array<string, string>, currentUri: string, expectedPattern: string}>
     */
    public function checkUriDataProvider(): Generator
    {
        yield 'simple path' => [
            [
                '/tickets' => '10,60,1',
            ],
            '/tickets',
            '/tickets',
        ];

        yield 'nested path' => [
            [
                '/tickets' => '10,60,1',
                '/tickets/users' => '5,30,0.5',
                '/tickets/users/nicknames' => '2,15,0.1',
            ],
            '/tickets/users/nicknames',
            '/tickets/users/nicknames',
        ];

        yield 'nested path with wildcard' => [
            [
                '/tickets' => '10,60,1',
                '/tickets/users' => '5,30,0.5',
                '/tickets/users/*' => '2,15,0.1',
            ],
            '/tickets/users/nicknames',
            '/tickets/users/*',
        ];

        yield 'nested path with param' => [
            [
                '/tickets' => '10,60,1',
                '/tickets/users' => '5,30,0.5',
                '/tickets/users/[0-9]+' => '2,15,0.1',
            ],
            '/tickets/users/123',
            '/tickets/users/[0-9]+',
        ];
    }
}
