<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Domain\Model\PasswordResetToken;

use App\Administration\Domain\Event\PasswordResetTokenGenerated;
use App\Administration\Domain\Event\PasswordResetTokenUsed;
use App\Administration\Domain\Exception\PasswordResetTokenAlreadyUsedException;
use App\Administration\Domain\Exception\PasswordResetTokenExpiredException;
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetToken;
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class PasswordResetTokenTest extends TestCase
{
    private TenantId $tenantId;
    private DateTimeImmutable $now;

    protected function setUp(): void
    {
        $this->tenantId = TenantId::generate();
        $this->now = new DateTimeImmutable('2026-01-28 10:00:00');
    }

    #[Test]
    public function itGeneratesANewToken(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        self::assertInstanceOf(PasswordResetTokenId::class, $token->id);
        self::assertNotEmpty($token->tokenValue);
        self::assertSame('user-123', $token->userId);
        self::assertSame('user@example.com', $token->email);
        self::assertTrue($token->tenantId->equals($this->tenantId));
        self::assertSame($this->now, $token->createdAt);
        self::assertNull($token->usedAt);
    }

    #[Test]
    public function itExpiresAfterOneHour(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $expectedExpiresAt = $this->now->modify('+1 hour');
        self::assertEquals($expectedExpiresAt, $token->expiresAt);
    }

    #[Test]
    public function itIsNotExpiredBeforeOneHour(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $fiftyNineMinutesLater = $this->now->modify('+59 minutes');
        self::assertFalse($token->isExpired($fiftyNineMinutesLater));
    }

    #[Test]
    public function itIsExpiredAfterOneHour(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $oneHourLater = $this->now->modify('+1 hour');
        self::assertTrue($token->isExpired($oneHourLater));
    }

    #[Test]
    public function itRecordsGenerationEvent(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $events = $token->pullDomainEvents();

        self::assertCount(1, $events);
        self::assertInstanceOf(PasswordResetTokenGenerated::class, $events[0]);

        /** @var PasswordResetTokenGenerated $event */
        $event = $events[0];
        self::assertTrue($event->tokenId->equals($token->id));
        self::assertSame('user-123', $event->userId);
        self::assertSame('user@example.com', $event->email);
        self::assertTrue($event->tenantId->equals($this->tenantId));
    }

    #[Test]
    public function itCanBeUsedWhenValid(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $token->pullDomainEvents(); // Clear generation event

        $useAt = $this->now->modify('+30 minutes');
        $token->use($useAt);

        self::assertSame($useAt, $token->usedAt);
        self::assertTrue($token->isUsed());
    }

    #[Test]
    public function itRecordsUsedEventWhenConsumed(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $token->pullDomainEvents(); // Clear generation event

        $useAt = $this->now->modify('+30 minutes');
        $token->use($useAt);

        $events = $token->pullDomainEvents();

        self::assertCount(1, $events);
        self::assertInstanceOf(PasswordResetTokenUsed::class, $events[0]);

        /** @var PasswordResetTokenUsed $event */
        $event = $events[0];
        self::assertTrue($event->tokenId->equals($token->id));
        self::assertSame('user-123', $event->userId);
    }

    #[Test]
    public function itCannotBeUsedTwice(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $useAt = $this->now->modify('+30 minutes');
        $token->use($useAt);

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

        $token->use($useAt->modify('+1 minute'));
    }

    #[Test]
    public function itCannotBeUsedWhenExpired(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

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

        $twoHoursLater = $this->now->modify('+2 hours');
        $token->use($twoHoursLater);
    }

    #[Test]
    public function itValidatesForUseWithoutConsuming(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $thirtyMinutesLater = $this->now->modify('+30 minutes');

        // Should not throw
        $token->validateForUse($thirtyMinutesLater);

        // Token should NOT be marked as used
        self::assertFalse($token->isUsed());
        self::assertNull($token->usedAt);
    }

    #[Test]
    public function validateForUseThrowsWhenExpired(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

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

        $twoHoursLater = $this->now->modify('+2 hours');
        $token->validateForUse($twoHoursLater);
    }

    #[Test]
    public function validateForUseThrowsWhenAlreadyUsed(): void
    {
        $token = PasswordResetToken::generate(
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
        );

        $token->use($this->now->modify('+30 minutes'));

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

        $token->validateForUse($this->now->modify('+31 minutes'));
    }

    #[Test]
    public function itCanBeReconstitutedFromStorage(): void
    {
        $id = PasswordResetTokenId::generate();
        $tokenValue = 'abc-123-def-456';
        $expiresAt = $this->now->modify('+1 hour');
        $usedAt = $this->now->modify('+30 minutes');

        $token = PasswordResetToken::reconstitute(
            id: $id,
            tokenValue: $tokenValue,
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
            expiresAt: $expiresAt,
            usedAt: $usedAt,
        );

        self::assertTrue($token->id->equals($id));
        self::assertSame($tokenValue, $token->tokenValue);
        self::assertSame('user-123', $token->userId);
        self::assertSame('user@example.com', $token->email);
        self::assertTrue($token->tenantId->equals($this->tenantId));
        self::assertSame($this->now, $token->createdAt);
        self::assertEquals($expiresAt, $token->expiresAt);
        self::assertSame($usedAt, $token->usedAt);
        self::assertTrue($token->isUsed());
    }

    #[Test]
    public function reconstituteDoesNotRecordEvents(): void
    {
        $token = PasswordResetToken::reconstitute(
            id: PasswordResetTokenId::generate(),
            tokenValue: 'abc-123',
            userId: 'user-123',
            email: 'user@example.com',
            tenantId: $this->tenantId,
            createdAt: $this->now,
            expiresAt: $this->now->modify('+1 hour'),
            usedAt: null,
        );

        $events = $token->pullDomainEvents();

        self::assertCount(0, $events);
    }
}
