<?php

declare(strict_types=1);

namespace Grav\Plugin\Api\Tests\Unit\Middleware;

use Grav\Plugin\Api\Middleware\RateLimitMiddleware;
use Grav\Plugin\Api\Tests\Unit\TestHelper;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
 * Tests for the token-bucket RateLimitMiddleware.
 *
 * The real class calls Grav::instance()['locator'] in getStorageDir().
 * We extend the class and override getStorageDir() to use a temp directory,
 * making the core checkLimit() logic testable in isolation.
 */
#[CoversClass(RateLimitMiddleware::class)]
class RateLimitMiddlewareTest extends TestCase
{
    private string $tempDir;

    protected function setUp(): void
    {
        $this->tempDir = sys_get_temp_dir() . '/grav_api_ratelimit_test_' . uniqid();
        @mkdir($this->tempDir, 0775, true);
    }

    protected function tearDown(): void
    {
        $files = glob($this->tempDir . '/*');
        if ($files) {
            array_map('unlink', $files);
        }
        @rmdir($this->tempDir);
    }

    #[Test]
    public function check_returns_not_limited_when_under_limit(): void
    {
        $middleware = $this->createTestableMiddleware(limit: 10, window: 60);
        $request = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '127.0.0.1'],
        );

        $result = $middleware->check($request);

        self::assertFalse($result['limited']);
        self::assertSame(10, $result['limit']);
        self::assertGreaterThanOrEqual(0, $result['remaining']);
    }

    #[Test]
    public function check_returns_limited_when_over_limit(): void
    {
        $middleware = $this->createTestableMiddleware(limit: 3, window: 60);
        $request = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '10.0.0.1'],
        );

        // Exhaust the 3 allowed tokens
        for ($i = 0; $i < 3; $i++) {
            $result = $middleware->check($request);
            self::assertFalse($result['limited'], "Request $i should not be limited");
        }

        // The 4th request should be rate-limited
        $result = $middleware->check($request);
        self::assertTrue($result['limited']);
        self::assertSame(0, $result['remaining']);
    }

    #[Test]
    public function rate_limit_disabled_always_allows(): void
    {
        $config = TestHelper::createMockConfig([
            'plugins' => ['api' => ['rate_limit' => [
                'enabled' => false,
                'requests' => 5,
                'window' => 60,
            ]]],
        ]);

        $tempDir = $this->tempDir;
        $middleware = new class ($config, $tempDir) extends RateLimitMiddleware {
            public function __construct(
                \Grav\Common\Config\Config $config,
                private readonly string $dir,
            ) {
                parent::__construct($config);
            }

            protected function getStorageDir(): string
            {
                return $this->dir;
            }
        };

        $request = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '192.168.1.1'],
        );

        // Even after many requests it should never be limited
        for ($i = 0; $i < 20; $i++) {
            $result = $middleware->check($request);
            self::assertFalse($result['limited']);
            self::assertSame(5, $result['remaining']);
        }
    }

    #[Test]
    public function tokens_refill_over_time(): void
    {
        $limit = 2;
        $window = 60;
        $middleware = $this->createTestableMiddleware(limit: $limit, window: $window);

        $request = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '172.16.0.1'],
        );

        // Exhaust all tokens
        for ($i = 0; $i < $limit; $i++) {
            $middleware->check($request);
        }

        // Manipulate the stored file to simulate time passing.
        $identifier = 'ip:172.16.0.1';
        $file = $this->tempDir . '/' . md5($identifier) . '.json';
        self::assertFileExists($file);

        $data = json_decode(file_get_contents($file), true);
        // Pretend last_refill was 30 seconds ago.
        // With limit=2, window=60: refill rate = 2/60 per second.
        // After 30s: refill = 30 * (2/60) = 1 token.
        $data['last_refill'] -= 30;
        file_put_contents($file, json_encode($data));

        $result = $middleware->check($request);
        self::assertFalse($result['limited'], 'Token bucket should have refilled after elapsed time');
    }

    #[Test]
    public function different_users_have_separate_limits(): void
    {
        $middleware = $this->createTestableMiddleware(limit: 2, window: 60);

        $requestA = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '10.0.0.1'],
        );
        $requestB = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '10.0.0.2'],
        );

        // Exhaust limit for user A
        for ($i = 0; $i < 2; $i++) {
            $middleware->check($requestA);
        }
        $resultA = $middleware->check($requestA);
        self::assertTrue($resultA['limited'], 'User A should be rate-limited');

        // User B should still have full budget
        $resultB = $middleware->check($requestB);
        self::assertFalse($resultB['limited'], 'User B should NOT be rate-limited');
    }

    #[Test]
    public function authenticated_user_identified_by_username(): void
    {
        $middleware = $this->createTestableMiddleware(limit: 1, window: 60);

        $user = TestHelper::createMockUser('alice');

        // Two different IPs but same authenticated user
        $requestFromOffice = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '10.0.0.1'],
            attributes: ['api_user' => $user],
        );
        $requestFromHome = TestHelper::createMockRequest(
            serverParams: ['REMOTE_ADDR' => '192.168.1.1'],
            attributes: ['api_user' => $user],
        );

        // First request uses the single token
        $result = $middleware->check($requestFromOffice);
        self::assertFalse($result['limited']);

        // Second request should also be limited (same user bucket)
        $result = $middleware->check($requestFromHome);
        self::assertTrue($result['limited']);
    }

    #[Test]
    public function only_sync_and_thumbnails_skip_the_bucket(): void
    {
        $paths = [
            '/api/v1/sync/channels/pull',
            '/sub/dir/api/v1/sync/presence',
            '/api/v1/thumbnails/0f1e2d3c4b5a69788796a5b4c3d2e1f0.jpg',
            '/sub/dir/api/v1/thumbnails/0f1e2d3c4b5a69788796a5b4c3d2e1f0.webp',
        ];
        foreach ($paths as $n => $path) {
            $middleware = $this->createTestableMiddleware(limit: 1, window: 60);
            for ($i = 0; $i < 3; $i++) {
                $request = TestHelper::createMockRequest(path: $path, serverParams: ['REMOTE_ADDR' => "10.9.9.{$n}"]);
                self::assertFalse($middleware->check($request)['limited'], "{$path} should skip rate limiting");
            }
        }
    }

    #[Test]
    public function plugin_script_routes_are_rate_limited(): void
    {
        $paths = [
            '/api/v1/custom-fields',
            '/api/v1/gpm/plugins/editor-pro/fields',
            '/api/v1/gpm/themes/quark/field/color',
            '/api/v1/gpm/plugins/ai-pro/widget-script',
            '/api/v1/gpm/plugins/reports/report-script/weekly',
        ];
        foreach ($paths as $n => $path) {
            $middleware = $this->createTestableMiddleware(limit: 1, window: 60);
            $request = TestHelper::createMockRequest(path: $path, serverParams: ['REMOTE_ADDR' => "10.6.6.{$n}"]);
            self::assertFalse($middleware->check($request)['limited']);
            self::assertTrue($middleware->check($request)['limited'], "{$path} should be rate limited");
        }
    }

    #[Test]
    public function paths_merely_containing_an_excluded_fragment_are_limited(): void
    {
        // These used to match str_contains('/sync/').
        foreach (['/api/v1/pages/sync/notes', '/api/v1/pages/docs/sync/intro', '/api/v1/pages/blog/thumbnails/gallery'] as $n => $path) {
            $middleware = $this->createTestableMiddleware(limit: 1, window: 60);
            $request = TestHelper::createMockRequest(path: $path, serverParams: ['REMOTE_ADDR' => "10.8.8.{$n}"]);
            self::assertFalse($middleware->check($request)['limited']);
            self::assertTrue($middleware->check($request)['limited'], "{$path} should be rate limited");
        }
    }

    /**
     * Build a testable RateLimitMiddleware subclass that uses a temp storage directory.
     */
    private function createTestableMiddleware(int $limit = 120, int $window = 60): RateLimitMiddleware
    {
        $config = TestHelper::createMockConfig([
            'plugins' => ['api' => ['rate_limit' => [
                'enabled' => true,
                'requests' => $limit,
                'window' => $window,
            ]]],
        ]);

        $tempDir = $this->tempDir;

        return new class ($config, $tempDir) extends RateLimitMiddleware {
            public function __construct(
                \Grav\Common\Config\Config $config,
                private readonly string $dir,
            ) {
                parent::__construct($config);
            }

            protected function getStorageDir(): string
            {
                return $this->dir;
            }
        };
    }
}
