<?php

declare(strict_types=1);

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

use App\Shared\Infrastructure\Captcha\TurnstileResult;
use App\Shared\Infrastructure\Captcha\TurnstileValidatorInterface;
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
use App\Shared\Infrastructure\RateLimit\LoginRateLimitListener;
use App\Shared\Infrastructure\RateLimit\LoginRateLimitResult;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

final class LoginRateLimitListenerTest extends TestCase
{
    private function createListener(
        ?LoginRateLimiterInterface $rateLimiter = null,
        ?TurnstileValidatorInterface $turnstile = null,
        ?CacheItemPoolInterface $cache = null,
    ): LoginRateLimitListener {
        return new LoginRateLimitListener(
            $rateLimiter ?? $this->createMock(LoginRateLimiterInterface::class),
            $turnstile ?? $this->createMock(TurnstileValidatorInterface::class),
            $cache ?? $this->createCacheMock(),
        );
    }

    private function createCacheMock(int $captchaFailures = 0): CacheItemPoolInterface
    {
        $cacheItem = $this->createMock(CacheItemInterface::class);
        $cacheItem->method('isHit')->willReturn($captchaFailures > 0);
        $cacheItem->method('get')->willReturn($captchaFailures);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')->willReturnSelf();

        $cache = $this->createMock(CacheItemPoolInterface::class);
        $cache->method('getItem')->willReturn($cacheItem);
        $cache->method('save')->willReturn(true);
        $cache->method('deleteItem')->willReturn(true);

        return $cache;
    }

    #[Test]
    public function blockedIpReturns429BeforeAuthentication(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::blocked(retryAfter: 600));

        $listener = $this->createListener(rateLimiter: $rateLimiter);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode(['email' => 'blocked@example.com', 'password' => 'correct'])
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        self::assertTrue($event->hasResponse());
        self::assertSame(Response::HTTP_TOO_MANY_REQUESTS, $event->getResponse()->getStatusCode());

        $content = json_decode($event->getResponse()->getContent(), true);
        self::assertSame('/errors/ip-blocked', $content['type']);
        self::assertSame(600, $content['retryAfter']);
    }

    #[Test]
    public function allowedEmailProceedsToAuthentication(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::allowed(
                attempts: 2,
                delaySeconds: 1,
                requiresCaptcha: false,
            ));

        $listener = $this->createListener(rateLimiter: $rateLimiter);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode(['email' => 'user@example.com', 'password' => 'password'])
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        // No response set = request continues to authentication
        self::assertFalse($event->hasResponse());
    }

    #[Test]
    public function captchaRequiredWithoutTokenReturns428(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::allowed(
                attempts: 6,
                delaySeconds: 8,
                requiresCaptcha: true,
            ));

        $listener = $this->createListener(rateLimiter: $rateLimiter);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode(['email' => 'user@example.com', 'password' => 'password'])
            // No captcha_token
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        self::assertTrue($event->hasResponse());
        self::assertSame(Response::HTTP_PRECONDITION_REQUIRED, $event->getResponse()->getStatusCode());

        $content = json_decode($event->getResponse()->getContent(), true);
        self::assertSame('/errors/captcha-required', $content['type']);
    }

    #[Test]
    public function captchaRequiredWithValidTokenProceeds(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::allowed(
                attempts: 6,
                delaySeconds: 8,
                requiresCaptcha: true,
            ));

        $turnstile = $this->createMock(TurnstileValidatorInterface::class);
        $turnstile->method('validate')
            ->willReturn(TurnstileResult::valid());

        $listener = $this->createListener(rateLimiter: $rateLimiter, turnstile: $turnstile);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode([
                'email' => 'user@example.com',
                'password' => 'password',
                'captcha_token' => 'valid-token',
            ])
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        // Should proceed to authentication
        self::assertFalse($event->hasResponse());
    }

    #[Test]
    public function captchaRequiredWithInvalidTokenReturns400(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::allowed(
                attempts: 6,
                delaySeconds: 8,
                requiresCaptcha: true,
            ));

        $turnstile = $this->createMock(TurnstileValidatorInterface::class);
        $turnstile->method('validate')
            ->willReturn(TurnstileResult::invalid('Token invalide'));

        $listener = $this->createListener(rateLimiter: $rateLimiter, turnstile: $turnstile);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode([
                'email' => 'user@example.com',
                'password' => 'password',
                'captcha_token' => 'invalid-token',
            ])
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        self::assertTrue($event->hasResponse());
        self::assertSame(Response::HTTP_BAD_REQUEST, $event->getResponse()->getStatusCode());

        $content = json_decode($event->getResponse()->getContent(), true);
        self::assertSame('/errors/captcha-invalid', $content['type']);
        self::assertSame('Token invalide', $content['detail']);
    }

    #[Test]
    public function captchaFailuresPersistAcrossRequests(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::allowed(
                attempts: 6,
                delaySeconds: 8,
                requiresCaptcha: true,
            ));

        $turnstile = $this->createMock(TurnstileValidatorInterface::class);
        $turnstile->method('validate')
            ->willReturn(TurnstileResult::invalid('Token invalide'));

        // Simulate 2 previous failures (next failure = 3 = blocked)
        $cache = $this->createCacheMock(captchaFailures: 2);

        $listener = $this->createListener(rateLimiter: $rateLimiter, turnstile: $turnstile, cache: $cache);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode([
                'email' => 'user@example.com',
                'password' => 'password',
                'captcha_token' => 'invalid-token',
            ])
        );
        $request->server->set('REMOTE_ADDR', '192.168.1.100');

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        // 3rd CAPTCHA failure should block the IP
        self::assertTrue($event->hasResponse());
        self::assertSame(Response::HTTP_TOO_MANY_REQUESTS, $event->getResponse()->getStatusCode());

        $content = json_decode($event->getResponse()->getContent(), true);
        self::assertSame('/errors/ip-blocked', $content['type']);
    }

    #[Test]
    public function cacheIsSavedOnCaptchaFailure(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::allowed(
                attempts: 6,
                delaySeconds: 8,
                requiresCaptcha: true,
            ));

        $turnstile = $this->createMock(TurnstileValidatorInterface::class);
        $turnstile->method('validate')
            ->willReturn(TurnstileResult::invalid('Token invalide'));

        $cacheItem = $this->createMock(CacheItemInterface::class);
        $cacheItem->method('isHit')->willReturn(false);
        $cacheItem->method('get')->willReturn(0);
        $cacheItem->expects(self::once())->method('set')->with(1)->willReturnSelf();
        $cacheItem->expects(self::once())->method('expiresAfter')->with(900)->willReturnSelf();

        $cache = $this->createMock(CacheItemPoolInterface::class);
        $cache->method('getItem')->willReturn($cacheItem);
        $cache->expects(self::once())->method('save')->with($cacheItem)->willReturn(true);

        $listener = $this->createListener(rateLimiter: $rateLimiter, turnstile: $turnstile, cache: $cache);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode([
                'email' => 'user@example.com',
                'password' => 'password',
                'captcha_token' => 'invalid-token',
            ])
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);
    }

    #[Test]
    public function cacheIsDeletedOnValidCaptcha(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->method('check')
            ->willReturn(LoginRateLimitResult::allowed(
                attempts: 6,
                delaySeconds: 8,
                requiresCaptcha: true,
            ));

        $turnstile = $this->createMock(TurnstileValidatorInterface::class);
        $turnstile->method('validate')
            ->willReturn(TurnstileResult::valid());

        $cache = $this->createMock(CacheItemPoolInterface::class);
        $cache->expects(self::once())->method('deleteItem');

        $listener = $this->createListener(rateLimiter: $rateLimiter, turnstile: $turnstile, cache: $cache);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode([
                'email' => 'user@example.com',
                'password' => 'password',
                'captcha_token' => 'valid-token',
            ])
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);
    }

    #[Test]
    public function ignoresNonLoginRequests(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->expects(self::never())->method('check');

        $listener = $this->createListener(rateLimiter: $rateLimiter);

        $request = Request::create('/api/users', 'GET');

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        self::assertFalse($event->hasResponse());
    }

    #[Test]
    public function ignoresLoginGetRequests(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->expects(self::never())->method('check');

        $listener = $this->createListener(rateLimiter: $rateLimiter);

        $request = Request::create('/api/login', 'GET');

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        self::assertFalse($event->hasResponse());
    }

    #[Test]
    public function proceedsIfEmailMissingFromRequest(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->expects(self::never())->method('check');

        $listener = $this->createListener(rateLimiter: $rateLimiter);

        $request = Request::create(
            '/api/login',
            'POST',
            [],
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode(['password' => 'password']) // No email
        );

        $kernel = $this->createMock(HttpKernelInterface::class);
        $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

        $listener($event);

        // Let the validator handle missing email
        self::assertFalse($event->hasResponse());
    }
}
