<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Application\Command\RequestPasswordReset;

use App\Administration\Application\Command\RequestPasswordReset\RequestPasswordResetCommand;
use App\Administration\Application\Command\RequestPasswordReset\RequestPasswordResetHandler;
use App\Administration\Domain\Event\PasswordResetTokenGenerated;
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetToken;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Policy\ConsentementParentalPolicy;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryPasswordResetTokenRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
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\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

final class RequestPasswordResetHandlerTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string EMAIL = 'user@example.com';

    private InMemoryUserRepository $userRepository;
    private InMemoryPasswordResetTokenRepository $tokenRepository;
    private Clock $clock;
    /** @var DomainEvent[] */
    private array $dispatchedEvents = [];
    private RequestPasswordResetHandler $handler;
    private TenantId $tenantId;

    protected function setUp(): void
    {
        $this->userRepository = new InMemoryUserRepository();
        $this->clock = new class implements Clock {
            public DateTimeImmutable $now;

            public function __construct()
            {
                $this->now = new DateTimeImmutable('2026-01-28 10:00:00');
            }

            #[Override]
            public function now(): DateTimeImmutable
            {
                return $this->now;
            }
        };

        $this->tokenRepository = new InMemoryPasswordResetTokenRepository($this->clock);

        $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->tenantId = TenantId::fromString(self::TENANT_ID);

        $this->handler = new RequestPasswordResetHandler(
            $this->userRepository,
            $this->tokenRepository,
            $this->clock,
            $eventBus,
        );
    }

    #[Test]
    public function itGeneratesTokenWhenUserExists(): void
    {
        $user = $this->createAndSaveUser(self::EMAIL);

        $command = new RequestPasswordResetCommand(
            email: self::EMAIL,
            tenantId: $this->tenantId,
        );

        ($this->handler)($command);

        // Verify token was created
        $token = $this->tokenRepository->findValidTokenForUser((string) $user->id);
        self::assertNotNull($token);
        self::assertSame(self::EMAIL, $token->email);
        self::assertTrue($token->tenantId->equals($this->tenantId));
    }

    #[Test]
    public function itDispatchesTokenGeneratedEvent(): void
    {
        $this->createAndSaveUser(self::EMAIL);

        $command = new RequestPasswordResetCommand(
            email: self::EMAIL,
            tenantId: $this->tenantId,
        );

        ($this->handler)($command);

        self::assertCount(1, $this->dispatchedEvents);
        self::assertInstanceOf(PasswordResetTokenGenerated::class, $this->dispatchedEvents[0]);
    }

    #[Test]
    public function itSilentlySucceedsWhenUserDoesNotExist(): void
    {
        $command = new RequestPasswordResetCommand(
            email: 'nonexistent@example.com',
            tenantId: $this->tenantId,
        );

        // Should NOT throw - silently succeeds
        ($this->handler)($command);

        // No events dispatched
        self::assertCount(0, $this->dispatchedEvents);
    }

    #[Test]
    public function itSilentlySucceedsWhenEmailIsInvalid(): void
    {
        $command = new RequestPasswordResetCommand(
            email: 'not-an-email',
            tenantId: $this->tenantId,
        );

        // Should NOT throw - silently succeeds
        ($this->handler)($command);

        // No events dispatched
        self::assertCount(0, $this->dispatchedEvents);
    }

    #[Test]
    public function itReusesExistingValidToken(): void
    {
        $user = $this->createAndSaveUser(self::EMAIL);

        // First request - creates token
        $command = new RequestPasswordResetCommand(
            email: self::EMAIL,
            tenantId: $this->tenantId,
        );

        ($this->handler)($command);

        $firstToken = $this->tokenRepository->findValidTokenForUser((string) $user->id);
        self::assertNotNull($firstToken);

        // Clear dispatched events
        $this->dispatchedEvents = [];

        // Second request - should NOT create new token
        ($this->handler)($command);

        // Same token should exist
        $secondToken = $this->tokenRepository->findValidTokenForUser((string) $user->id);
        self::assertNotNull($secondToken);
        self::assertSame($firstToken->tokenValue, $secondToken->tokenValue);
    }

    #[Test]
    public function itCreatesNewTokenWhenExistingTokenIsExpired(): void
    {
        $user = $this->createAndSaveUser(self::EMAIL);

        // Create an expired token manually
        $expiredToken = PasswordResetToken::generate(
            userId: (string) $user->id,
            email: self::EMAIL,
            tenantId: $this->tenantId,
            createdAt: new DateTimeImmutable('2026-01-28 08:00:00'), // 2 hours ago
        );
        $this->tokenRepository->save($expiredToken);

        // findValidTokenForUser should return null for expired tokens
        $validToken = $this->tokenRepository->findValidTokenForUser((string) $user->id);
        self::assertNull($validToken);

        // Now request a new token
        $command = new RequestPasswordResetCommand(
            email: self::EMAIL,
            tenantId: $this->tenantId,
        );

        ($this->handler)($command);

        // A new token should be created
        self::assertCount(1, $this->dispatchedEvents);
        self::assertInstanceOf(PasswordResetTokenGenerated::class, $this->dispatchedEvents[0]);
    }

    #[Test]
    public function itDoesNotGenerateTokenForUserInDifferentTenant(): void
    {
        // Create user in tenant 1
        $this->createAndSaveUser(self::EMAIL);

        // Request reset for different tenant
        $differentTenantId = TenantId::fromString('550e8400-e29b-41d4-a716-446655440099');
        $command = new RequestPasswordResetCommand(
            email: self::EMAIL,
            tenantId: $differentTenantId,
        );

        ($this->handler)($command);

        // No events dispatched (user not found in different tenant)
        self::assertCount(0, $this->dispatchedEvents);
    }

    private function createAndSaveUser(string $email): User
    {
        $user = User::creer(
            email: new Email($email),
            role: Role::PROF,
            tenantId: $this->tenantId,
            schoolName: 'École Test',
            dateNaissance: new DateTimeImmutable('1990-01-01'),
            createdAt: $this->clock->now(),
        );

        // Activate user so they can request password reset
        $consentementPolicy = new ConsentementParentalPolicy($this->clock);
        $user->activer(
            hashedPassword: '$argon2id$hashed',
            at: $this->clock->now(),
            consentementPolicy: $consentementPolicy,
        );

        $this->userRepository->save($user);

        return $user;
    }
}
