<?php

declare(strict_types=1);

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

use ApiPlatform\Metadata\Post;
use App\Administration\Application\Command\ActivateAccount\ActivateAccountHandler;
use App\Administration\Application\Command\LinkParentToStudent\LinkParentToStudentHandler;
use App\Administration\Application\Port\PasswordHasher;
use App\Administration\Domain\Exception\UserNotFoundException;
use App\Administration\Domain\Model\ActivationToken\ActivationToken;
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\Model\User\UserId;
use App\Administration\Domain\Policy\ConsentementParentalPolicy;
use App\Administration\Domain\Repository\UserRepository;
use App\Administration\Infrastructure\Api\Processor\ActivateAccountProcessor;
use App\Administration\Infrastructure\Api\Resource\ActivateAccountInput;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryActivationTokenRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * Tests for ActivateAccountProcessor focusing on token consumption behavior.
 *
 * Key invariant: Failed activations must NOT consume the token, allowing retries.
 */
final class ActivateAccountProcessorTest extends TestCase
{
    private const string USER_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string EMAIL = 'user@example.com';
    private const string ROLE = 'ROLE_PARENT';
    private const string SCHOOL_NAME = 'École Test';
    private const string PASSWORD = 'SecurePass123';
    private const string HASHED_PASSWORD = '$argon2id$hashed';

    private InMemoryActivationTokenRepository $tokenRepository;
    private Clock $clock;

    protected function setUp(): void
    {
        $this->tokenRepository = new InMemoryActivationTokenRepository();
        $this->clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-01-16 10:00:00');
            }
        };
    }

    #[Test]
    public function tokenRemainsValidWhenUserNotFound(): void
    {
        // Arrange: Create a valid token
        $token = $this->createAndSaveToken();
        $tokenValue = $token->tokenValue;

        // Create processor with a UserRepository that throws UserNotFoundException
        $processor = $this->createProcessorWithMissingUser();

        $input = new ActivateAccountInput();
        $input->tokenValue = $tokenValue;
        $input->password = self::PASSWORD;

        // Act: Try to activate (should fail because user not found)
        try {
            $processor->process($input, new Post());
            self::fail('Expected NotFoundHttpException to be thrown');
        } catch (NotFoundHttpException $e) {
            self::assertSame('Utilisateur introuvable.', $e->getMessage());
        }

        // Assert: Token should NOT be consumed - retry should be possible
        $tokenAfterFailure = $this->tokenRepository->findByTokenValue($tokenValue);
        self::assertNotNull($tokenAfterFailure, 'Token should still exist after failed activation');
        self::assertFalse($tokenAfterFailure->isUsed(), 'Token should NOT be marked as used after failed activation');
    }

    #[Test]
    public function tokenCanBeReusedAfterFailedActivation(): void
    {
        // Arrange: Create a valid token
        $token = $this->createAndSaveToken();
        $tokenValue = $token->tokenValue;

        $processorWithMissingUser = $this->createProcessorWithMissingUser();

        $input = new ActivateAccountInput();
        $input->tokenValue = $tokenValue;
        $input->password = self::PASSWORD;

        // Act: First activation fails (user not found)
        try {
            $processorWithMissingUser->process($input, new Post());
        } catch (NotFoundHttpException) {
            // Expected
        }

        // Assert: Can call handler again with same token (retry scenario)
        $handler = $this->createHandler();
        $result = ($handler)(new \App\Administration\Application\Command\ActivateAccount\ActivateAccountCommand(
            tokenValue: $tokenValue,
            password: self::PASSWORD,
        ));

        // Should succeed - token was not burned
        self::assertSame(self::USER_ID, $result->userId);
    }

    private function createAndSaveToken(): ActivationToken
    {
        $token = ActivationToken::generate(
            userId: self::USER_ID,
            email: self::EMAIL,
            tenantId: TenantId::fromString(self::TENANT_ID),
            role: self::ROLE,
            schoolName: self::SCHOOL_NAME,
            createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
        );

        $this->tokenRepository->save($token);

        return $token;
    }

    private function createHandler(): ActivateAccountHandler
    {
        $passwordHasher = new class implements PasswordHasher {
            public function hash(string $plainPassword): string
            {
                return '$argon2id$hashed';
            }

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

        return new ActivateAccountHandler(
            $this->tokenRepository,
            $passwordHasher,
            $this->clock,
        );
    }

    private function createProcessorWithMissingUser(): ActivateAccountProcessor
    {
        $handler = $this->createHandler();

        // UserRepository that always throws UserNotFoundException
        $userRepository = new class implements UserRepository {
            public function save(User $user): void
            {
            }

            public function findById(UserId $id): ?User
            {
                return null;
            }

            public function findByEmail(Email $email, TenantId $tenantId): ?User
            {
                return null;
            }

            public function get(UserId $id): User
            {
                throw UserNotFoundException::withId($id);
            }

            public function findAllByTenant(TenantId $tenantId): array
            {
                return [];
            }

            public function findStudentsByTenant(TenantId $tenantId): array
            {
                return [];
            }
        };

        $consentementPolicy = new ConsentementParentalPolicy($this->clock);

        $eventBus = $this->createMock(MessageBusInterface::class);

        $linkHandler = new LinkParentToStudentHandler(
            new InMemoryStudentGuardianRepository(),
            $userRepository,
            $this->clock,
        );

        return new ActivateAccountProcessor(
            $handler,
            $userRepository,
            $this->tokenRepository,
            $consentementPolicy,
            $this->clock,
            $eventBus,
            $linkHandler,
            new NullLogger(),
        );
    }
}
