<?php

declare(strict_types=1);

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

use App\Administration\Domain\Model\RefreshToken\DeviceFingerprint;
use App\Administration\Domain\Model\RefreshToken\RefreshToken;
use App\Administration\Domain\Model\User\UserId;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class RefreshTokenTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';

    #[Test]
    public function createGeneratesTokenWithCorrectData(): void
    {
        $userId = UserId::generate();
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $fingerprint = DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1');
        $issuedAt = new DateTimeImmutable('2026-01-31 10:00:00');

        $token = RefreshToken::create($userId, $tenantId, $fingerprint, $issuedAt);

        self::assertSame($userId, $token->userId);
        self::assertSame($tenantId, $token->tenantId);
        self::assertTrue($token->deviceFingerprint->equals($fingerprint));
        self::assertEquals($issuedAt, $token->issuedAt);
        self::assertNull($token->rotatedFrom);
        self::assertFalse($token->isRotated);
    }

    #[Test]
    public function createSetsExpirationBasedOnTtl(): void
    {
        $issuedAt = new DateTimeImmutable('2026-01-31 10:00:00');
        $ttl = 86400; // 1 day

        $token = RefreshToken::create(
            UserId::generate(),
            TenantId::fromString(self::TENANT_ID),
            DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'),
            $issuedAt,
            $ttl,
        );

        $expectedExpiry = $issuedAt->modify('+86400 seconds');
        self::assertEquals($expectedExpiry, $token->expiresAt);
    }

    #[Test]
    public function rotateCreatesNewTokenWithSameFamily(): void
    {
        $token = $this->createToken();
        $rotateAt = new DateTimeImmutable('2026-01-31 11:00:00');

        [$newToken, $oldToken] = $token->rotate($rotateAt);

        // Nouveau token
        self::assertNotSame($token->id, $newToken->id);
        self::assertSame($token->familyId, $newToken->familyId);
        self::assertSame($token->userId, $newToken->userId);
        self::assertSame($token->id, $newToken->rotatedFrom);
        self::assertFalse($newToken->isRotated);

        // Ancien token marqué comme rotaté
        self::assertSame($token->id, $oldToken->id);
        self::assertTrue($oldToken->isRotated);
    }

    #[Test]
    public function isExpiredReturnsTrueWhenPastExpiration(): void
    {
        $issuedAt = new DateTimeImmutable('2026-01-31 10:00:00');
        $token = RefreshToken::create(
            UserId::generate(),
            TenantId::fromString(self::TENANT_ID),
            DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'),
            $issuedAt,
            3600, // 1 hour
        );

        self::assertFalse($token->isExpired(new DateTimeImmutable('2026-01-31 10:30:00')));
        self::assertTrue($token->isExpired(new DateTimeImmutable('2026-01-31 11:30:00')));
    }

    #[Test]
    public function isInGracePeriodReturnsTrueWithin30SecondsOfRotation(): void
    {
        $token = $this->createToken();
        $rotateAt = new DateTimeImmutable('2026-01-31 11:00:00');

        [$_, $oldToken] = $token->rotate($rotateAt);

        // Dans la grace period (30s après rotation)
        self::assertTrue($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:00:15')));
        self::assertTrue($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:00:30')));

        // Après la grace period
        self::assertFalse($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:00:31')));
        self::assertFalse($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:01:00')));
    }

    #[Test]
    public function rotatePreservesOriginalTtl(): void
    {
        $issuedAt = new DateTimeImmutable('2026-01-31 10:00:00');
        $originalTtl = 86400; // 1 day (web session)

        $token = RefreshToken::create(
            UserId::generate(),
            TenantId::fromString(self::TENANT_ID),
            DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'),
            $issuedAt,
            $originalTtl,
        );

        $rotateAt = new DateTimeImmutable('2026-01-31 14:00:00');
        [$newToken, $oldToken] = $token->rotate($rotateAt);

        // Le nouveau token doit avoir le même TTL que l'original
        $expectedExpiry = $rotateAt->modify("+{$originalTtl} seconds");
        self::assertEquals($expectedExpiry, $newToken->expiresAt);

        // L'ancien token garde son expiration originale
        self::assertEquals($issuedAt->modify("+{$originalTtl} seconds"), $oldToken->expiresAt);

        // L'ancien token a rotatedAt défini
        self::assertEquals($rotateAt, $oldToken->rotatedAt);
        self::assertNull($newToken->rotatedAt);
    }

    #[Test]
    public function matchesDeviceReturnsTrueForSameFingerprint(): void
    {
        $fingerprint = DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1');
        $token = RefreshToken::create(
            UserId::generate(),
            TenantId::fromString(self::TENANT_ID),
            $fingerprint,
            new DateTimeImmutable(),
        );

        $sameFingerprint = DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1');
        $differentFingerprint = DeviceFingerprint::fromRequest('Chrome/110', '10.0.0.1');

        self::assertTrue($token->matchesDevice($sameFingerprint));
        self::assertFalse($token->matchesDevice($differentFingerprint));
    }

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

        $tokenString = $token->toTokenString();
        $extractedId = RefreshToken::extractIdFromTokenString($tokenString);

        self::assertEquals($token->id, $extractedId);
    }

    private function createToken(): RefreshToken
    {
        return RefreshToken::create(
            UserId::generate(),
            TenantId::fromString(self::TENANT_ID),
            DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'),
            new DateTimeImmutable('2026-01-31 10:00:00'),
        );
    }
}
