<?php

declare(strict_types=1);

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

use App\Administration\Application\Command\ActivateAccount\ActivateAccountCommand;
use App\Administration\Application\Command\ActivateAccount\ActivateAccountHandler;
use App\Administration\Application\Command\ActivateAccount\ActivateAccountResult;
use App\Administration\Application\Port\PasswordHasher;
use App\Administration\Domain\Exception\ActivationTokenAlreadyUsedException;
use App\Administration\Domain\Exception\ActivationTokenExpiredException;
use App\Administration\Domain\Exception\ActivationTokenNotFoundException;
use App\Administration\Domain\Model\ActivationToken\ActivationToken;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryActivationTokenRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Override;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class ActivateAccountHandlerTest 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 Alpha';
    private const string PASSWORD = 'SecurePass123';
    private const string HASHED_PASSWORD = '$argon2id$hashed_password';

    private InMemoryActivationTokenRepository $tokenRepository;
    private PasswordHasher $passwordHasher;
    private Clock $clock;
    private ActivateAccountHandler $handler;

    protected function setUp(): void
    {
        $this->tokenRepository = new InMemoryActivationTokenRepository();
        $this->passwordHasher = new class implements PasswordHasher {
            #[Override]
            public function hash(string $plainPassword): string
            {
                return '$argon2id$hashed_password';
            }

            #[Override]
            public function verify(string $hashedPassword, string $plainPassword): bool
            {
                return true;
            }
        };
        $this->clock = new class implements Clock {
            public DateTimeImmutable $now;

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

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

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

    #[Test]
    public function activateAccountSuccessfully(): void
    {
        $token = $this->createAndSaveToken();

        $command = new ActivateAccountCommand(
            tokenValue: $token->tokenValue,
            password: self::PASSWORD,
        );

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

        self::assertInstanceOf(ActivateAccountResult::class, $result);
        self::assertSame(self::USER_ID, $result->userId);
        self::assertSame(self::EMAIL, $result->email);
        self::assertSame(self::ROLE, $result->role);
        self::assertSame(self::HASHED_PASSWORD, $result->hashedPassword);
    }

    #[Test]
    public function activateAccountValidatesButDoesNotConsumeToken(): void
    {
        // Handler only validates the token - consumption is deferred to the processor
        // after successful user activation, so failed activations don't burn the token
        $token = $this->createAndSaveToken();
        $tokenValue = $token->tokenValue;

        $command = new ActivateAccountCommand(
            tokenValue: $tokenValue,
            password: self::PASSWORD,
        );

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

        // Token should still exist and NOT be marked as used
        $updatedToken = $this->tokenRepository->findByTokenValue($tokenValue);
        self::assertNotNull($updatedToken);
        self::assertFalse($updatedToken->isUsed());
    }

    #[Test]
    public function activateAccountThrowsWhenTokenNotFound(): void
    {
        $command = new ActivateAccountCommand(
            tokenValue: 'non-existent-token',
            password: self::PASSWORD,
        );

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

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

    #[Test]
    public function activateAccountThrowsWhenTokenExpired(): void
    {
        $token = $this->createAndSaveToken(
            createdAt: new DateTimeImmutable('2026-01-01 10:00:00'),
        );

        // Clock is set to 2026-01-16, token expires 2026-01-08
        $command = new ActivateAccountCommand(
            tokenValue: $token->tokenValue,
            password: self::PASSWORD,
        );

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

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

    #[Test]
    public function activateAccountThrowsWhenTokenAlreadyUsed(): void
    {
        $token = $this->createAndSaveToken();

        // Simulate a token that was already used (e.g., by the processor after successful activation)
        $token->use($this->clock->now());
        $this->tokenRepository->save($token);

        $command = new ActivateAccountCommand(
            tokenValue: $token->tokenValue,
            password: self::PASSWORD,
        );

        // Should fail because token is already used
        $this->expectException(ActivationTokenAlreadyUsedException::class);

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

    #[Test]
    public function activateAccountCarriesStudentIdFromToken(): void
    {
        $studentId = '550e8400-e29b-41d4-a716-446655440099';
        $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'),
            studentId: $studentId,
        );
        $this->tokenRepository->save($token);

        $command = new ActivateAccountCommand(
            tokenValue: $token->tokenValue,
            password: self::PASSWORD,
        );

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

        self::assertSame($studentId, $result->studentId);
    }

    #[Test]
    public function activateAccountReturnsNullStudentIdWhenNotSet(): void
    {
        $token = $this->createAndSaveToken();

        $command = new ActivateAccountCommand(
            tokenValue: $token->tokenValue,
            password: self::PASSWORD,
        );

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

        self::assertNull($result->studentId);
    }

    private function createAndSaveToken(?DateTimeImmutable $createdAt = null): 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: $createdAt ?? new DateTimeImmutable('2026-01-15 10:00:00'),
        );

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

        return $token;
    }
}
