<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Tests\MessageHandler;

use Ferienpass\CmsBundle\Message\PasswordResetHelpRequested;
use Ferienpass\CmsBundle\Message\ResetPassword as ResetPasswordCommand;
use Ferienpass\CmsBundle\MessageHandler\ResetPassword;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\UserRepository;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;

final class ResetPasswordTest extends TestCase
{
    public function testDispatchesPasswordResetHelpWhenUserNotFound(): void
    {
        $repo = $this->createMock(UserRepository::class);
        $repo->method('findOneBy')->willReturn(null);

        $helper = $this->createMock(ResetPasswordHelperInterface::class);
        $helper->expects(self::never())->method('generateResetToken');

        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')
            ->with(self::callback(static fn (object $m): bool => $m instanceof PasswordResetHelpRequested && 'unknown@example.com' === $m->getEmail()))
            ->willReturn(new Envelope(new \stdClass()));

        $handler = new ResetPassword($repo, $helper, $bus, new NullLogger());
        $handler(new ResetPasswordCommand('unknown@example.com'));
    }

    public function testDispatchesPasswordLinkCreatedOnSuccess(): void
    {
        $user = new User();
        // Give the account a password so the handler proceeds to token
        // generation rather than diverting into the passkey-only branch.
        $user->setPassword('hashed-password');

        $repo = $this->createMock(UserRepository::class);
        $repo->method('findOneBy')->willReturn($user);

        $token = new ResetPasswordToken('my-reset-token', new \DateTime('+1 hour'));

        $helper = $this->createMock(ResetPasswordHelperInterface::class);
        $helper->method('generateResetToken')->willReturn($token);

        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));

        $handler = new ResetPassword($repo, $helper, $bus, new NullLogger());
        $handler(new ResetPasswordCommand('user@example.com'));
    }

    public function testSilentlyReturnsWhenResetPasswordExceptionThrown(): void
    {
        $user = new User();
        // Give the account a password so the handler proceeds to token
        // generation rather than diverting into the passkey-only branch.
        $user->setPassword('hashed-password');

        $repo = $this->createMock(UserRepository::class);
        $repo->method('findOneBy')->willReturn($user);

        $exception = new class('error') extends \RuntimeException implements ResetPasswordExceptionInterface {
            public function getReason(): string
            {
                return $this->getMessage();
            }
        };

        $helper = $this->createMock(ResetPasswordHelperInterface::class);
        $helper->method('generateResetToken')->willThrowException($exception);

        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::never())->method('dispatch');

        $handler = new ResetPassword($repo, $helper, $bus, new NullLogger());
        $handler(new ResetPasswordCommand('user@example.com'));

        $this->addToAssertionCount(1);
    }

    public function testRecordsTheOutcomeWhenTheRequestIsThrottled(): void
    {
        $user = new User();
        $user->setPassword('hashed-password');

        $repo = $this->createMock(UserRepository::class);
        $repo->method('findOneBy')->willReturn($user);

        $exception = new class('too many requests') extends \RuntimeException implements ResetPasswordExceptionInterface {
            public function getReason(): string
            {
                return $this->getMessage();
            }
        };

        $helper = $this->createMock(ResetPasswordHelperInterface::class);
        $helper->method('generateResetToken')->willThrowException($exception);

        $bus = $this->createMock(MessageBusInterface::class);
        $bus->expects(self::never())->method('dispatch');

        // The requester is told to check their inbox either way, so the log is
        // the only way to tell a throttled request from a delivered one.
        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects(self::once())->method('info')
            ->with(
                self::anything(),
                self::callback(static fn (array $c): bool => 'throttled' === $c['outcome']
                    && 'user@example.com' === $c['email']
                    && 'too many requests' === $c['reason']),
            );

        $handler = new ResetPassword($repo, $helper, $bus, $logger);
        $handler(new ResetPasswordCommand('user@example.com'));
    }
}
