<?php

declare(strict_types=1);

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

use ApiPlatform\Metadata\Post;
use App\Administration\Application\Command\ResetPassword\ResetPasswordHandler;
use App\Administration\Application\Port\PasswordHasher;
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\Infrastructure\Api\Processor\ResetPasswordProcessor;
use App\Administration\Infrastructure\Api\Resource\ResetPasswordInput;
use App\Administration\Infrastructure\Api\Resource\ResetPasswordOutput;
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\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * Tests for ResetPasswordProcessor.
 *
 * Key invariants:
 * - Token not found -> 400
 * - Token expired -> 410
 * - Token already used -> 410
 * - Success -> ResetPasswordOutput with message
 */
final class ResetPasswordProcessorTest extends TestCase
{
    private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
    private const string USER_ID = '550e8400-e29b-41d4-a716-446655440001';

    private InMemoryUserRepository $userRepository;
    private InMemoryPasswordResetTokenRepository $tokenRepository;
    private InMemoryRefreshTokenRepository $refreshTokenRepository;
    private Clock $clock;
    private MessageBusInterface $eventBus;
    private PasswordHasher $passwordHasher;

    protected function setUp(): void
    {
        $this->clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-15 10:00:00');
            }
        };
        $this->userRepository = new InMemoryUserRepository();
        $this->tokenRepository = new InMemoryPasswordResetTokenRepository($this->clock);
        $this->refreshTokenRepository = new InMemoryRefreshTokenRepository();
        $this->eventBus = $this->createMock(MessageBusInterface::class);
        $this->passwordHasher = new class implements PasswordHasher {
            public function hash(string $plainPassword): string
            {
                return '$argon2id$hashed_new';
            }

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

    #[Test]
    public function returnsSuccessOnValidReset(): void
    {
        $this->saveUser();
        $token = $this->saveValidToken();

        $this->eventBus->method('dispatch')
            ->willReturnCallback(static fn (object $msg) => new Envelope($msg));

        $processor = $this->createProcessor();

        $input = new ResetPasswordInput();
        $input->token = $token->tokenValue;
        $input->password = 'NewSecureP@ss1';

        $output = $processor->process($input, new Post());

        self::assertInstanceOf(ResetPasswordOutput::class, $output);
        self::assertStringContainsString('succ', $output->message);
    }

    #[Test]
    public function throwsBadRequestWhenTokenNotFound(): void
    {
        $processor = $this->createProcessor();

        $input = new ResetPasswordInput();
        $input->token = 'nonexistent-token';
        $input->password = 'SecureP@ss1';

        $this->expectException(BadRequestHttpException::class);
        $this->expectExceptionMessage('invalide');

        $processor->process($input, new Post());
    }

    #[Test]
    public function throwsGoneWhenTokenExpired(): void
    {
        $this->saveUser();

        // Create a token that is already expired (created 2 hours ago, 1 hour TTL)
        $token = PasswordResetToken::generate(
            userId: self::USER_ID,
            email: 'user@example.com',
            tenantId: TenantId::fromString(self::TENANT_ID),
            createdAt: new DateTimeImmutable('2026-02-15 07:00:00'), // 3 hours ago
        );
        $this->tokenRepository->save($token);

        $processor = $this->createProcessor();

        $input = new ResetPasswordInput();
        $input->token = $token->tokenValue;
        $input->password = 'SecureP@ss1';

        $this->expectException(GoneHttpException::class);
        $this->expectExceptionMessage('expir');

        $processor->process($input, new Post());
    }

    #[Test]
    public function throwsGoneWhenTokenAlreadyUsed(): void
    {
        $this->saveUser();
        $token = $this->saveValidToken();

        // Use the token once
        $this->tokenRepository->consumeIfValid($token->tokenValue, $this->clock->now());

        $this->eventBus->method('dispatch')
            ->willReturnCallback(static fn (object $msg) => new Envelope($msg));

        $processor = $this->createProcessor();

        $input = new ResetPasswordInput();
        $input->token = $token->tokenValue;
        $input->password = 'SecureP@ss1';

        $this->expectException(GoneHttpException::class);
        $this->expectExceptionMessage('utilisé');

        $processor->process($input, new Post());
    }

    private function createProcessor(): ResetPasswordProcessor
    {
        $handler = new ResetPasswordHandler(
            $this->tokenRepository,
            $this->userRepository,
            $this->refreshTokenRepository,
            $this->passwordHasher,
            $this->clock,
            $this->eventBus,
        );

        return new ResetPasswordProcessor($handler);
    }

    private function saveUser(): void
    {
        $user = User::reconstitute(
            id: \App\Administration\Domain\Model\User\UserId::fromString(self::USER_ID),
            email: new Email('user@example.com'),
            roles: [Role::PARENT],
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: 'Ecole Test',
            statut: \App\Administration\Domain\Model\User\StatutCompte::ACTIF,
            dateNaissance: null,
            createdAt: new DateTimeImmutable('2026-01-15T10:00:00+00:00'),
            hashedPassword: '$argon2id$old_hash',
            activatedAt: new DateTimeImmutable('2026-01-16T10:00:00+00:00'),
            consentementParental: null,
            firstName: 'Jean',
            lastName: 'Dupont',
        );
        $this->userRepository->save($user);
    }

    private function saveValidToken(): PasswordResetToken
    {
        $token = PasswordResetToken::generate(
            userId: self::USER_ID,
            email: 'user@example.com',
            tenantId: TenantId::fromString(self::TENANT_ID),
            createdAt: new DateTimeImmutable('2026-02-15 09:30:00'), // 30 min ago, within 1h TTL
        );
        $this->tokenRepository->save($token);

        return $token;
    }
}
