<?php

declare(strict_types=1);

namespace App\Tests\Unit\Shared\Infrastructure\Audit\Handler;

use App\Administration\Domain\Event\CompteBloqueTemporairement;
use App\Administration\Domain\Event\ConnexionEchouee;
use App\Administration\Domain\Event\ConnexionReussie;
use App\Administration\Domain\Event\MotDePasseChange;
use App\Shared\Application\Port\AuditLogger;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Audit\Handler\AuditAuthenticationHandler;
use DateTimeImmutable;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;

/**
 * @see Story 1.7 - T4: Listeners Authentification
 */
final class AuditAuthenticationHandlerTest extends TestCase
{
    private AuditLogger&MockObject $auditLogger;
    private AuditAuthenticationHandler $handler;

    protected function setUp(): void
    {
        $this->auditLogger = $this->createMock(AuditLogger::class);
        $this->handler = new AuditAuthenticationHandler(
            $this->auditLogger,
            'test-secret',
        );
    }

    public function testHandleConnexionReussieLogsSuccessfulLogin(): void
    {
        $userId = Uuid::uuid4()->toString();
        $event = new ConnexionReussie(
            userId: $userId,
            email: 'user@example.com',
            tenantId: TenantId::generate(),
            ipAddress: '192.168.1.1',
            userAgent: 'Mozilla/5.0',
            occurredOn: new DateTimeImmutable(),
        );

        $this->auditLogger->expects($this->once())
            ->method('logAuthentication')
            ->with(
                $this->equalTo('ConnexionReussie'),
                $this->callback(static fn ($uuid) => $uuid->toString() === $userId),
                $this->callback(static fn ($payload) => $payload['result'] === 'success'
                    && $payload['method'] === 'password'
                    && isset($payload['email_hash'])
                ),
            );

        $this->handler->handleConnexionReussie($event);
    }

    public function testHandleConnexionEchoueeLogsFailedLogin(): void
    {
        $tenantId = TenantId::generate();
        $event = new ConnexionEchouee(
            email: 'user@example.com',
            tenantId: $tenantId,
            ipAddress: '192.168.1.1',
            userAgent: 'Mozilla/5.0',
            reason: 'invalid_credentials',
            occurredOn: new DateTimeImmutable(),
        );

        $this->auditLogger->expects($this->once())
            ->method('logAuthentication')
            ->with(
                $this->equalTo('ConnexionEchouee'),
                $this->isNull(),
                $this->callback(static fn ($payload) => $payload['result'] === 'failure'
                    && $payload['reason'] === 'invalid_credentials'
                    && isset($payload['email_hash'])
                ),
                $this->equalTo((string) $tenantId),
            );

        $this->handler->handleConnexionEchouee($event);
    }

    public function testHandleCompteBloqueTemporairementLogsLockout(): void
    {
        $tenantId = TenantId::generate();
        $event = new CompteBloqueTemporairement(
            email: 'user@example.com',
            tenantId: $tenantId,
            ipAddress: '192.168.1.1',
            userAgent: 'Mozilla/5.0',
            blockedForSeconds: 300,
            failedAttempts: 5,
            occurredOn: new DateTimeImmutable(),
        );

        $this->auditLogger->expects($this->once())
            ->method('logAuthentication')
            ->with(
                $this->equalTo('CompteBloqueTemporairement'),
                $this->isNull(),
                $this->callback(static fn ($payload) => $payload['blocked_for_seconds'] === 300
                    && $payload['failed_attempts'] === 5
                    && isset($payload['email_hash'])
                ),
                $this->equalTo((string) $tenantId),
            );

        $this->handler->handleCompteBloqueTemporairement($event);
    }

    public function testHandleMotDePasseChangeLogsPasswordChange(): void
    {
        $userId = Uuid::uuid4()->toString();
        $event = new MotDePasseChange(
            userId: $userId,
            email: 'user@example.com',
            tenantId: TenantId::generate(),
            occurredOn: new DateTimeImmutable(),
        );

        $this->auditLogger->expects($this->once())
            ->method('logAuthentication')
            ->with(
                $this->equalTo('MotDePasseChange'),
                $this->callback(static fn ($uuid) => $uuid->toString() === $userId),
                $this->callback(static fn ($payload) => isset($payload['email_hash'])),
            );

        $this->handler->handleMotDePasseChange($event);
    }

    public function testEmailIsHashedConsistently(): void
    {
        $email = 'user@example.com';
        $expectedHash = hash('sha256', strtolower($email) . 'test-secret');

        $capturedPayload = null;
        $this->auditLogger->expects($this->once())
            ->method('logAuthentication')
            ->willReturnCallback(static function ($eventType, $userId, $payload) use (&$capturedPayload) {
                $capturedPayload = $payload;
            });

        $event = new ConnexionReussie(
            userId: Uuid::uuid4()->toString(),
            email: $email,
            tenantId: TenantId::generate(),
            ipAddress: '192.168.1.1',
            userAgent: 'Mozilla/5.0',
            occurredOn: new DateTimeImmutable(),
        );

        $this->handler->handleConnexionReussie($event);

        $this->assertSame($expectedHash, $capturedPayload['email_hash']);
    }

    public function testEmailHashIsCaseInsensitive(): void
    {
        $lowerEmail = 'user@example.com';
        $upperEmail = 'USER@EXAMPLE.COM';
        $expectedHash = hash('sha256', strtolower($lowerEmail) . 'test-secret');

        $payloads = [];
        $this->auditLogger->expects($this->exactly(2))
            ->method('logAuthentication')
            ->willReturnCallback(static function ($eventType, $userId, $payload) use (&$payloads) {
                $payloads[] = $payload;
            });

        $event1 = new ConnexionReussie(
            userId: Uuid::uuid4()->toString(),
            email: $lowerEmail,
            tenantId: TenantId::generate(),
            ipAddress: '192.168.1.1',
            userAgent: 'Mozilla/5.0',
            occurredOn: new DateTimeImmutable(),
        );

        $event2 = new ConnexionReussie(
            userId: Uuid::uuid4()->toString(),
            email: $upperEmail,
            tenantId: TenantId::generate(),
            ipAddress: '192.168.1.1',
            userAgent: 'Mozilla/5.0',
            occurredOn: new DateTimeImmutable(),
        );

        $this->handler->handleConnexionReussie($event1);
        $this->handler->handleConnexionReussie($event2);

        $this->assertSame($payloads[0]['email_hash'], $payloads[1]['email_hash']);
    }
}
