<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Api\Controller;

use App\Administration\Domain\Event\Deconnexion;
use App\Administration\Domain\Model\RefreshToken\DeviceFingerprint;
use App\Administration\Domain\Model\RefreshToken\RefreshToken;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\RefreshTokenRepository;
use App\Administration\Domain\Repository\SessionRepository;
use App\Administration\Infrastructure\Api\Controller\LogoutController;
use App\Shared\Domain\Clock;
use App\Shared\Domain\DomainEvent;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Override;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * [P0] Tests for LogoutController - critical session invalidation.
 *
 * Verifies:
 * - Token family invalidation on logout
 * - Session deletion
 * - Cookie clearing
 * - Deconnexion event dispatch
 * - Graceful handling of missing/malformed tokens
 */
final class LogoutControllerTest 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 IP_ADDRESS = '192.168.1.100';
    private const string USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';

    private RefreshTokenRepository $refreshTokenRepository;
    private SessionRepository $sessionRepository;
    private Clock $clock;
    /** @var DomainEvent[] */
    private array $dispatchedEvents = [];
    private LogoutController $controller;

    protected function setUp(): void
    {
        $this->refreshTokenRepository = $this->createMock(RefreshTokenRepository::class);
        $this->sessionRepository = $this->createMock(SessionRepository::class);

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

        $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->controller = new LogoutController(
            $this->refreshTokenRepository,
            $this->sessionRepository,
            $eventBus,
            $this->clock,
        );
    }

    #[Test]
    public function itInvalidatesTokenFamilyOnLogout(): void
    {
        // GIVEN: A request with a valid refresh token cookie
        $refreshToken = $this->createRefreshToken();
        $tokenString = $refreshToken->toTokenString();

        $request = Request::create('/api/token/logout', 'POST', [], [
            'refresh_token' => $tokenString,
        ], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);

        $this->refreshTokenRepository
            ->expects($this->once())
            ->method('find')
            ->with($refreshToken->id)
            ->willReturn($refreshToken);

        // THEN: Family should be invalidated
        $this->refreshTokenRepository
            ->expects($this->once())
            ->method('invalidateFamily')
            ->with($refreshToken->familyId);

        // WHEN: Logout is invoked
        $response = ($this->controller)($request);

        // THEN: Returns success
        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
    }

    #[Test]
    public function itDeletesSessionOnLogout(): void
    {
        // GIVEN: A valid refresh token
        $refreshToken = $this->createRefreshToken();

        $request = Request::create('/api/token/logout', 'POST', [], [
            'refresh_token' => $refreshToken->toTokenString(),
        ], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);

        $this->refreshTokenRepository
            ->method('find')
            ->willReturn($refreshToken);

        // THEN: Session should be deleted
        $this->sessionRepository
            ->expects($this->once())
            ->method('delete')
            ->with($refreshToken->familyId);

        // WHEN: Logout is invoked
        ($this->controller)($request);
    }

    #[Test]
    public function itDispatchesDeconnexionEvent(): void
    {
        // GIVEN: A valid refresh token
        $refreshToken = $this->createRefreshToken();

        $request = Request::create('/api/token/logout', 'POST', [], [
            'refresh_token' => $refreshToken->toTokenString(),
        ], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);

        $this->refreshTokenRepository
            ->method('find')
            ->willReturn($refreshToken);

        // WHEN: Logout is invoked
        ($this->controller)($request);

        // THEN: Deconnexion event is dispatched with correct data
        $logoutEvents = array_filter(
            $this->dispatchedEvents,
            static fn ($e) => $e instanceof Deconnexion,
        );
        $this->assertCount(1, $logoutEvents);

        $event = reset($logoutEvents);
        $this->assertSame((string) $refreshToken->userId, $event->userId);
        $this->assertSame((string) $refreshToken->familyId, $event->familyId);
        $this->assertSame(self::IP_ADDRESS, $event->ipAddress);
        $this->assertSame(self::USER_AGENT, $event->userAgent);
    }

    #[Test]
    public function itClearsCookiesOnLogout(): void
    {
        // GIVEN: A valid refresh token
        $refreshToken = $this->createRefreshToken();

        $request = Request::create('/api/token/logout', 'POST', [], [
            'refresh_token' => $refreshToken->toTokenString(),
        ], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);

        $this->refreshTokenRepository
            ->method('find')
            ->willReturn($refreshToken);

        // WHEN: Logout is invoked
        $response = ($this->controller)($request);

        // THEN: Cookies are cleared (expired)
        $cookies = $response->headers->getCookies();
        $this->assertCount(3, $cookies); // refresh_token /api, /api/token (legacy), classeo_sid

        $cookieNames = array_map(static fn ($c) => $c->getName(), $cookies);
        $this->assertContains('refresh_token', $cookieNames);
        $this->assertContains('classeo_sid', $cookieNames);

        foreach ($cookies as $cookie) {
            $this->assertSame('', $cookie->getValue());
            $this->assertTrue($cookie->isCleared()); // Expiry in the past
        }
    }

    #[Test]
    public function itHandlesMissingCookieGracefully(): void
    {
        // GIVEN: A request without refresh_token cookie
        $request = Request::create('/api/token/logout', 'POST', [], [], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);

        // THEN: No repository operations
        $this->refreshTokenRepository
            ->expects($this->never())
            ->method('find');

        $this->refreshTokenRepository
            ->expects($this->never())
            ->method('invalidateFamily');

        $this->sessionRepository
            ->expects($this->never())
            ->method('delete');

        // WHEN: Logout is invoked
        $response = ($this->controller)($request);

        // THEN: Still returns success (idempotent)
        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());

        // THEN: Cookies are still cleared
        $this->assertNotEmpty($response->headers->getCookies());
    }

    #[Test]
    public function itHandlesMalformedTokenGracefully(): void
    {
        // GIVEN: A request with malformed token
        $request = Request::create('/api/token/logout', 'POST', [], [
            'refresh_token' => 'malformed-token-not-base64',
        ], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);

        // THEN: No repository operations (exception caught)
        $this->refreshTokenRepository
            ->expects($this->never())
            ->method('invalidateFamily');

        // WHEN: Logout is invoked
        $response = ($this->controller)($request);

        // THEN: Still returns success
        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
    }

    #[Test]
    public function itHandlesNonExistentTokenGracefully(): void
    {
        // GIVEN: A valid token format but not in database
        $refreshToken = $this->createRefreshToken();

        $request = Request::create('/api/token/logout', 'POST', [], [
            'refresh_token' => $refreshToken->toTokenString(),
        ], [], [
            'REMOTE_ADDR' => self::IP_ADDRESS,
            'HTTP_USER_AGENT' => self::USER_AGENT,
        ]);

        $this->refreshTokenRepository
            ->method('find')
            ->willReturn(null);

        // THEN: No invalidation attempted
        $this->refreshTokenRepository
            ->expects($this->never())
            ->method('invalidateFamily');

        // WHEN: Logout is invoked
        $response = ($this->controller)($request);

        // THEN: Still returns success (idempotent)
        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
    }

    private function createRefreshToken(): RefreshToken
    {
        return RefreshToken::create(
            userId: UserId::fromString(self::USER_ID),
            tenantId: TenantId::fromString(self::TENANT_ID),
            deviceFingerprint: DeviceFingerprint::fromRequest(self::USER_AGENT, self::IP_ADDRESS),
            issuedAt: $this->clock->now(),
        );
    }
}
