<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Security;

use App\Administration\Infrastructure\Security\LoginFailureHandler;
use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Monitoring\MetricsCollector;
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
use App\Shared\Infrastructure\RateLimit\LoginRateLimitResult;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantNotFoundException;
use App\Shared\Infrastructure\Tenant\TenantResolver;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\InMemory;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;

final class LoginFailureHandlerTest extends TestCase
{
    private function createMetricsCollector(): MetricsCollector
    {
        return new MetricsCollector(
            new CollectorRegistry(new InMemory()),
            new TenantContext(),
        );
    }

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

        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->expects(self::never())->method('dispatch');

        $clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-07 10:00:00');
            }
        };

        $tenantResolver = $this->createMock(TenantResolver::class);

        $handler = new LoginFailureHandler($rateLimiter, $eventBus, $clock, $tenantResolver, $this->createMetricsCollector());

        $request = Request::create('/api/login', 'POST', [], [], [], [], json_encode(['email' => 'blocked@example.com', 'password' => 'test']));
        $exception = new CustomUserMessageAccountStatusException('Votre compte a été suspendu. Contactez votre établissement.');

        $response = $handler->onAuthenticationFailure($request, $exception);

        self::assertSame(403, $response->getStatusCode());
        $data = json_decode($response->getContent(), true);
        self::assertSame('/errors/account-suspended', $data['type']);
        self::assertSame('Compte suspendu', $data['title']);
    }

    #[Test]
    public function standardFailureReturns401WithRateLimiting(): void
    {
        $rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $rateLimiter->expects(self::once())
            ->method('recordFailure')
            ->willReturn(LoginRateLimitResult::allowed(1, 0, false));

        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->method('dispatch')->willReturnCallback(
            static fn (object $message) => new Envelope($message),
        );

        $clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-07 10:00:00');
            }
        };

        $tenantResolver = $this->createMock(TenantResolver::class);
        $tenantResolver->method('resolve')->willThrowException(TenantNotFoundException::withSubdomain('unknown'));

        $handler = new LoginFailureHandler($rateLimiter, $eventBus, $clock, $tenantResolver, $this->createMetricsCollector());

        $request = Request::create('/api/login', 'POST', [], [], [], [], json_encode(['email' => 'user@example.com', 'password' => 'wrong']));
        $exception = new AuthenticationException('Invalid credentials.');

        $response = $handler->onAuthenticationFailure($request, $exception);

        self::assertSame(401, $response->getStatusCode());
        $data = json_decode($response->getContent(), true);
        self::assertSame('/errors/authentication-failed', $data['type']);
    }
}
