<?php

declare(strict_types=1);

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

use App\Administration\Application\Port\GeoLocationService;
use App\Administration\Application\Service\RefreshTokenManager;
use App\Administration\Domain\Event\ConnexionReussie;
use App\Administration\Domain\Model\Session\Location;
use App\Administration\Domain\Model\Session\Session;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\SessionRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryRefreshTokenRepository;
use App\Administration\Infrastructure\Security\LoginSuccessHandler;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Shared\Domain\Clock;
use App\Shared\Domain\DomainEvent;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
use DateTimeImmutable;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Override;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * [P0] Tests for LoginSuccessHandler - critical authentication flow.
 *
 * Verifies:
 * - Refresh token creation on successful login
 * - Session creation with device info and geolocation
 * - Rate limiter reset after successful login
 * - ConnexionReussie event dispatch
 * - HttpOnly cookie configuration
 */
final class LoginSuccessHandlerTest extends TestCase
{
    private const string USER_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string EMAIL = 'user@example.com';
    private const string IP_ADDRESS = '192.168.1.100';
    private const string USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';

    private InMemoryRefreshTokenRepository $refreshTokenRepository;
    private RefreshTokenManager $refreshTokenManager;
    private SessionRepository $sessionRepository;
    private GeoLocationService $geoLocationService;
    private LoginRateLimiterInterface $rateLimiter;
    private Clock $clock;
    private RequestStack $requestStack;
    /** @var DomainEvent[] */
    private array $dispatchedEvents = [];
    private LoginSuccessHandler $handler;

    protected function setUp(): void
    {
        $this->clock = new class implements Clock {
            #[Override]
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-01-28 10:00:00');
            }
        };

        $this->refreshTokenRepository = new InMemoryRefreshTokenRepository();
        $this->refreshTokenManager = new RefreshTokenManager(
            $this->refreshTokenRepository,
            $this->clock,
        );

        $this->sessionRepository = $this->createMock(SessionRepository::class);
        $this->geoLocationService = $this->createMock(GeoLocationService::class);
        $this->rateLimiter = $this->createMock(LoginRateLimiterInterface::class);
        $this->requestStack = new RequestStack();

        $this->dispatchedEvents = [];
        $eventBus = new class($this->dispatchedEvents) implements MessageBusInterface {
            /** @param DomainEvent[] $events */
            public function __construct(private array &$events)
            {
            }

            #[Override]
            public function dispatch(object $message, array $stamps = []): Envelope
            {
                $this->events[] = $message;

                return new Envelope($message);
            }
        };

        $this->handler = new LoginSuccessHandler(
            $this->refreshTokenManager,
            $this->sessionRepository,
            $this->geoLocationService,
            $this->rateLimiter,
            $eventBus,
            $this->clock,
            $this->requestStack,
        );
    }

    #[Test]
    public function itCreatesRefreshTokenOnSuccessfulLogin(): void
    {
        // GIVEN: A successful authentication event with SecurityUser
        $request = $this->createRequest();
        $this->requestStack->push($request);

        $securityUser = $this->createSecurityUser();
        $response = new Response();
        $event = new AuthenticationSuccessEvent(['token' => 'jwt'], $securityUser, $response);

        $this->geoLocationService
            ->method('locate')
            ->willReturn(Location::unknown());

        $this->sessionRepository
            ->expects($this->once())
            ->method('save')
            ->with(
                $this->isInstanceOf(Session::class),
                $this->greaterThan(0),
            );

        // WHEN: Handler processes the event
        $this->handler->onAuthenticationSuccess($event);

        // THEN: Refresh token cookie and session ID cookie are set
        $cookies = $response->headers->getCookies();
        $this->assertCount(2, $cookies);
        $cookieNames = array_map(static fn ($c) => $c->getName(), $cookies);
        $this->assertContains('refresh_token', $cookieNames);
        $this->assertContains('classeo_sid', $cookieNames);

        // THEN: Refresh token is saved in repository
        $this->assertTrue(
            $this->refreshTokenRepository->hasActiveSessionsForUser(
                UserId::fromString(self::USER_ID),
            ),
        );
    }

    #[Test]
    public function itCreatesSessionWithDeviceInfoAndLocation(): void
    {
        // GIVEN: A successful authentication with request context
        $request = $this->createRequest();
        $this->requestStack->push($request);

        $securityUser = $this->createSecurityUser();
        $response = new Response();
        $event = new AuthenticationSuccessEvent(['token' => 'jwt'], $securityUser, $response);

        $expectedLocation = Location::fromIp(self::IP_ADDRESS, 'France', 'Paris');

        $this->geoLocationService
            ->expects($this->once())
            ->method('locate')
            ->with(self::IP_ADDRESS)
            ->willReturn($expectedLocation);

        $savedSession = null;
        $this->sessionRepository
            ->expects($this->once())
            ->method('save')
            ->willReturnCallback(static function (Session $session, int $ttl) use (&$savedSession): void {
                $savedSession = $session;
            });

        // WHEN: Handler processes the event
        $this->handler->onAuthenticationSuccess($event);

        // THEN: Session is created with correct data
        $this->assertNotNull($savedSession);
        $this->assertSame(self::USER_ID, (string) $savedSession->userId);
    }

    #[Test]
    public function itResetsRateLimiterAfterSuccessfulLogin(): void
    {
        // GIVEN: A successful authentication
        $request = $this->createRequest();
        $this->requestStack->push($request);

        $securityUser = $this->createSecurityUser();
        $response = new Response();
        $event = new AuthenticationSuccessEvent(['token' => 'jwt'], $securityUser, $response);

        $this->geoLocationService
            ->method('locate')
            ->willReturn(Location::unknown());

        // THEN: Rate limiter should be reset for the user's email
        $this->rateLimiter
            ->expects($this->once())
            ->method('reset')
            ->with(self::EMAIL);

        // WHEN: Handler processes the event
        $this->handler->onAuthenticationSuccess($event);
    }

    #[Test]
    public function itDispatchesConnexionReussieEvent(): void
    {
        // GIVEN: A successful authentication
        $request = $this->createRequest();
        $this->requestStack->push($request);

        $securityUser = $this->createSecurityUser();
        $response = new Response();
        $event = new AuthenticationSuccessEvent(['token' => 'jwt'], $securityUser, $response);

        $this->geoLocationService
            ->method('locate')
            ->willReturn(Location::unknown());

        // WHEN: Handler processes the event
        $this->handler->onAuthenticationSuccess($event);

        // THEN: ConnexionReussie event is dispatched
        $loginEvents = array_filter(
            $this->dispatchedEvents,
            static fn ($e) => $e instanceof ConnexionReussie,
        );
        $this->assertCount(1, $loginEvents);

        $loginEvent = reset($loginEvents);
        $this->assertSame(self::USER_ID, $loginEvent->userId);
        $this->assertSame(self::EMAIL, $loginEvent->email);
        $this->assertSame(self::IP_ADDRESS, $loginEvent->ipAddress);
    }

    #[Test]
    public function itIgnoresNonSecurityUserAuthentication(): void
    {
        // GIVEN: An authentication event with a non-SecurityUser user
        $request = $this->createRequest();
        $this->requestStack->push($request);

        $genericUser = $this->createMock(UserInterface::class);
        $response = new Response();
        $event = new AuthenticationSuccessEvent(['token' => 'jwt'], $genericUser, $response);

        // THEN: No operations should be performed
        $this->sessionRepository
            ->expects($this->never())
            ->method('save');

        // WHEN: Handler processes the event
        $this->handler->onAuthenticationSuccess($event);

        // THEN: No cookies added, no events dispatched
        $this->assertEmpty($response->headers->getCookies());
        $this->assertEmpty($this->dispatchedEvents);
    }

    #[Test]
    public function itIgnoresEventWithoutRequest(): void
    {
        // GIVEN: No request in the stack
        $securityUser = $this->createSecurityUser();
        $response = new Response();
        $event = new AuthenticationSuccessEvent(['token' => 'jwt'], $securityUser, $response);

        // THEN: No operations should be performed
        $this->sessionRepository
            ->expects($this->never())
            ->method('save');

        // WHEN: Handler processes the event
        $this->handler->onAuthenticationSuccess($event);

        // THEN: No cookies added
        $this->assertEmpty($response->headers->getCookies());
    }

    #[Test]
    public function itSetsSecureCookieOnlyForHttps(): void
    {
        // GIVEN: An HTTP request (not HTTPS)
        $request = Request::create('http://localhost/login', 'POST', [], [], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);
        $this->requestStack->push($request);

        $securityUser = $this->createSecurityUser();
        $response = new Response();
        $event = new AuthenticationSuccessEvent(['token' => 'jwt'], $securityUser, $response);

        $this->geoLocationService
            ->method('locate')
            ->willReturn(Location::unknown());

        // WHEN: Handler processes the event
        $this->handler->onAuthenticationSuccess($event);

        // THEN: Cookies are NOT marked as secure (HTTP)
        $cookies = $response->headers->getCookies();
        $this->assertCount(2, $cookies);
        foreach ($cookies as $cookie) {
            $this->assertFalse($cookie->isSecure());
        }
    }

    private function createRequest(): Request
    {
        return Request::create('/login', 'POST', [], [], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);
    }

    private function createSecurityUser(): SecurityUser
    {
        return new SecurityUser(
            userId: UserId::fromString(self::USER_ID),
            email: self::EMAIL,
            hashedPassword: '$argon2id$hashed',
            tenantId: TenantId::fromString(self::TENANT_ID),
            roles: ['ROLE_PROF'],
        );
    }
}
