<?php

declare(strict_types=1);

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

use App\Administration\Application\Command\ResetPassword\ResetPasswordCommand;
use App\Administration\Application\Command\ResetPassword\ResetPasswordHandler;
use App\Administration\Application\Port\PasswordHasher;
use App\Administration\Domain\Event\MotDePasseChange;
use App\Administration\Domain\Event\PasswordResetTokenUsed;
use App\Administration\Domain\Exception\PasswordResetTokenAlreadyUsedException;
use App\Administration\Domain\Exception\PasswordResetTokenExpiredException;
use App\Administration\Domain\Exception\PasswordResetTokenNotFoundException;
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetToken;
use App\Administration\Domain\Model\RefreshToken\DeviceFingerprint;
use App\Administration\Domain\Model\RefreshToken\RefreshToken;
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\InMemoryRefreshTokenRepository;
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 ResetPasswordHandlerTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string EMAIL = 'user@example.com';
    private const string NEW_PASSWORD = 'NewSecurePassword123!';
    public const string HASHED_PASSWORD = '$argon2id$newhashedpassword';

    private InMemoryUserRepository $userRepository;
    private InMemoryPasswordResetTokenRepository $tokenRepository;
    private InMemoryRefreshTokenRepository $refreshTokenRepository;
    private Clock $clock;
    /** @var DomainEvent[] */
    private array $dispatchedEvents = [];
    private ResetPasswordHandler $handler;
    private TenantId $tenantId;
    private PasswordHasher $passwordHasher;

    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->refreshTokenRepository = new InMemoryRefreshTokenRepository();

        $this->passwordHasher = new class implements PasswordHasher {
            #[Override]
            public function hash(string $plainPassword): string
            {
                return ResetPasswordHandlerTest::HASHED_PASSWORD;
            }

            #[Override]
            public function verify(string $hashedPassword, string $plainPassword): bool
            {
                return true;
            }
        };

        $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 ResetPasswordHandler(
            $this->tokenRepository,
            $this->userRepository,
            $this->refreshTokenRepository,
            $this->passwordHasher,
            $this->clock,
            $eventBus,
        );
    }

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

        $command = new ResetPasswordCommand(
            token: $token->tokenValue,
            newPassword: self::NEW_PASSWORD,
        );

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

        // Verify password was updated
        $updatedUser = $this->userRepository->get($user->id);
        self::assertSame(self::HASHED_PASSWORD, $updatedUser->hashedPassword);
    }

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

        $command = new ResetPasswordCommand(
            token: $token->tokenValue,
            newPassword: self::NEW_PASSWORD,
        );

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

        // Should have MotDePasseChange and PasswordResetTokenUsed events
        $passwordChangedEvents = array_filter(
            $this->dispatchedEvents,
            static fn ($e) => $e instanceof MotDePasseChange
        );
        self::assertCount(1, $passwordChangedEvents);
    }

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

        $command = new ResetPasswordCommand(
            token: $token->tokenValue,
            newPassword: self::NEW_PASSWORD,
        );

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

        $tokenUsedEvents = array_filter(
            $this->dispatchedEvents,
            static fn ($e) => $e instanceof PasswordResetTokenUsed
        );
        self::assertCount(1, $tokenUsedEvents);
    }

    #[Test]
    public function itThrowsWhenTokenNotFound(): void
    {
        $command = new ResetPasswordCommand(
            token: 'nonexistent-token',
            newPassword: self::NEW_PASSWORD,
        );

        $this->expectException(PasswordResetTokenNotFoundException::class);

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

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

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

        $command = new ResetPasswordCommand(
            token: $token->tokenValue,
            newPassword: self::NEW_PASSWORD,
        );

        $this->expectException(PasswordResetTokenExpiredException::class);

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

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

        $command = new ResetPasswordCommand(
            token: $token->tokenValue,
            newPassword: self::NEW_PASSWORD,
        );

        // First use succeeds
        ($this->handler)($command);

        // Second attempt with same token should fail with "already used"
        // (token remains in storage until TTL expiry to preserve this error semantic)
        $this->expectException(PasswordResetTokenAlreadyUsedException::class);

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

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

        $command = new ResetPasswordCommand(
            token: $tokenValue,
            newPassword: self::NEW_PASSWORD,
        );

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

        // Token should remain in storage (marked as used) until TTL expiry
        // This allows distinguishing "already used" (410) from "invalid" (400)
        $foundToken = $this->tokenRepository->findByTokenValue($tokenValue);
        self::assertNotNull($foundToken);
        self::assertTrue($foundToken->isUsed());
    }

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

        // Create active refresh tokens for the user (simulating active sessions)
        $refreshToken = RefreshToken::create(
            userId: $user->id,
            tenantId: $this->tenantId,
            deviceFingerprint: DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'),
            issuedAt: $this->clock->now(),
        );
        $this->refreshTokenRepository->save($refreshToken);

        // Verify user has active sessions before reset
        self::assertTrue($this->refreshTokenRepository->hasActiveSessionsForUser($user->id));

        $command = new ResetPasswordCommand(
            token: $token->tokenValue,
            newPassword: self::NEW_PASSWORD,
        );

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

        // All sessions should be invalidated after password reset (AC3)
        self::assertFalse($this->refreshTokenRepository->hasActiveSessionsForUser($user->id));
    }

    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
        $consentementPolicy = new ConsentementParentalPolicy($this->clock);
        $user->activer(
            hashedPassword: '$argon2id$oldhashed',
            at: $this->clock->now(),
            consentementPolicy: $consentementPolicy,
        );

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

        return $user;
    }

    private function createAndSaveToken(User $user): PasswordResetToken
    {
        $token = PasswordResetToken::generate(
            userId: (string) $user->id,
            email: self::EMAIL,
            tenantId: $this->tenantId,
            createdAt: $this->clock->now(),
        );
        // Drain events to avoid counting generation event
        $token->pullDomainEvents();
        $this->tokenRepository->save($token);

        return $token;
    }
}
