<?php

declare(strict_types=1);

namespace App\Tests\Unit\Shared\Infrastructure\RateLimit;

use App\Shared\Infrastructure\RateLimit\LoginRateLimiter;
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\HttpFoundation\Request;

final class LoginRateLimiterTest extends TestCase
{
    private ArrayAdapter $cache;
    private LoginRateLimiter $rateLimiter;

    protected function setUp(): void
    {
        $this->cache = new ArrayAdapter();
        $this->rateLimiter = new LoginRateLimiter($this->cache);
    }

    #[Test]
    public function checkReturnsAllowedForFirstAttempt(): void
    {
        $request = $this->createRequest('192.168.1.1');

        $result = $this->rateLimiter->check($request, 'test@example.com');

        self::assertTrue($result->isAllowed);
        self::assertFalse($result->ipBlocked);
        self::assertSame(0, $result->attempts);
        self::assertSame(0, $result->delaySeconds);
        self::assertFalse($result->requiresCaptcha);
    }

    #[Test]
    public function recordFailureIncrementsAttemptsAndCalculatesFibonacciDelay(): void
    {
        $request = $this->createRequest('192.168.1.1');
        $email = 'test@example.com';

        // First failure - no delay (1 attempt = 0s)
        $result = $this->rateLimiter->recordFailure($request, $email);
        self::assertSame(1, $result->attempts);
        self::assertSame(0, $result->delaySeconds);

        // Second failure - delay 1s (F0)
        $result = $this->rateLimiter->recordFailure($request, $email);
        self::assertSame(2, $result->attempts);
        self::assertSame(1, $result->delaySeconds);

        // Third failure - delay 1s (F1)
        $result = $this->rateLimiter->recordFailure($request, $email);
        self::assertSame(3, $result->attempts);
        self::assertSame(1, $result->delaySeconds);

        // Fourth failure - delay 2s (F2)
        $result = $this->rateLimiter->recordFailure($request, $email);
        self::assertSame(4, $result->attempts);
        self::assertSame(2, $result->delaySeconds);

        // Fifth failure - delay 3s (F3), CAPTCHA required
        $result = $this->rateLimiter->recordFailure($request, $email);
        self::assertSame(5, $result->attempts);
        self::assertSame(3, $result->delaySeconds);
        self::assertTrue($result->requiresCaptcha);
    }

    #[Test]
    public function checkReturnsCorrectStateAfterFailures(): void
    {
        $request = $this->createRequest('192.168.1.1');
        $email = 'test@example.com';

        // Record 5 failures
        for ($i = 0; $i < 5; ++$i) {
            $this->rateLimiter->recordFailure($request, $email);
        }

        // Check should return the current state
        $result = $this->rateLimiter->check($request, $email);
        self::assertSame(5, $result->attempts);
        self::assertTrue($result->requiresCaptcha);
    }

    #[Test]
    public function blockIpPreventsSubsequentAttempts(): void
    {
        $ip = '192.168.1.1';
        $request = $this->createRequest($ip);

        $this->rateLimiter->blockIp($ip);

        $result = $this->rateLimiter->check($request, 'any@email.com');

        self::assertTrue($result->ipBlocked);
        self::assertFalse($result->isAllowed);
        self::assertGreaterThan(0, $result->retryAfter);
    }

    #[Test]
    public function recordFailureBlocksIpAfter20Attempts(): void
    {
        $request = $this->createRequest('192.168.1.1');
        $email = 'attacker@example.com';

        // Record 19 failures - should not be blocked
        for ($i = 0; $i < 19; ++$i) {
            $result = $this->rateLimiter->recordFailure($request, $email);
            self::assertFalse($result->ipBlocked);
        }

        // 20th failure - should be blocked
        $result = $this->rateLimiter->recordFailure($request, $email);
        self::assertTrue($result->ipBlocked);
        self::assertSame(LoginRateLimiterInterface::IP_BLOCK_DURATION, $result->retryAfter);
    }

    #[Test]
    public function resetClearsAttemptsForEmail(): void
    {
        $request = $this->createRequest('192.168.1.1');
        $email = 'test@example.com';

        // Record some failures
        $this->rateLimiter->recordFailure($request, $email);
        $this->rateLimiter->recordFailure($request, $email);

        // Reset
        $this->rateLimiter->reset($email);

        // Check should show 0 attempts
        $result = $this->rateLimiter->check($request, $email);
        self::assertSame(0, $result->attempts);
    }

    #[Test]
    public function isIpBlockedReturnsFalseForUnblockedIp(): void
    {
        self::assertFalse($this->rateLimiter->isIpBlocked('192.168.1.1'));
    }

    #[Test]
    public function isIpBlockedReturnsTrueForBlockedIp(): void
    {
        $ip = '192.168.1.1';
        $this->rateLimiter->blockIp($ip);

        self::assertTrue($this->rateLimiter->isIpBlocked($ip));
    }

    #[Test]
    public function differentEmailsHaveSeparateAttemptCounters(): void
    {
        $request = $this->createRequest('192.168.1.1');

        // Record failures for email1
        $this->rateLimiter->recordFailure($request, 'email1@test.com');
        $this->rateLimiter->recordFailure($request, 'email1@test.com');

        // Record failure for email2
        $this->rateLimiter->recordFailure($request, 'email2@test.com');

        // Check each email
        $result1 = $this->rateLimiter->check($request, 'email1@test.com');
        $result2 = $this->rateLimiter->check($request, 'email2@test.com');

        self::assertSame(2, $result1->attempts);
        self::assertSame(1, $result2->attempts);
    }

    #[Test]
    public function emailNormalizationIsCaseInsensitive(): void
    {
        $request = $this->createRequest('192.168.1.1');

        $this->rateLimiter->recordFailure($request, 'Test@Example.COM');
        $this->rateLimiter->recordFailure($request, 'test@example.com');

        $result = $this->rateLimiter->check($request, 'TEST@EXAMPLE.COM');

        self::assertSame(2, $result->attempts);
    }

    private function createRequest(string $clientIp): Request
    {
        $request = Request::create('/api/login', 'POST');
        $request->server->set('REMOTE_ADDR', $clientIp);

        return $request;
    }
}
