<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Persistence\InMemory;

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

/**
 * [P1] Tests for InMemoryRefreshTokenRepository.
 *
 * Verifies:
 * - Token save and retrieval
 * - Token deletion
 * - Family invalidation (all tokens in family)
 * - User invalidation (all families for user)
 * - Index maintenance
 */
final class InMemoryRefreshTokenRepositoryTest extends TestCase
{
    private const string USER_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string USER_ID_2 = '550e8400-e29b-41d4-a716-446655440003';
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';

    private InMemoryRefreshTokenRepository $repository;
    private DateTimeImmutable $now;

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

    #[Test]
    public function itSavesAndRetrievesToken(): void
    {
        // GIVEN: A refresh token
        $token = $this->createToken(self::USER_ID);

        // WHEN: Token is saved
        $this->repository->save($token);

        // THEN: Token can be retrieved by ID
        $found = $this->repository->find($token->id);
        $this->assertNotNull($found);
        $this->assertSame((string) $token->id, (string) $found->id);
        $this->assertSame((string) $token->familyId, (string) $found->familyId);
        $this->assertSame((string) $token->userId, (string) $found->userId);
    }

    #[Test]
    public function itReturnsNullForNonExistentToken(): void
    {
        // GIVEN: Empty repository

        // WHEN: Searching for non-existent token
        $token = $this->createToken(self::USER_ID);
        $found = $this->repository->find($token->id);

        // THEN: Returns null
        $this->assertNull($found);
    }

    #[Test]
    public function itDeletesTokenById(): void
    {
        // GIVEN: A saved token
        $token = $this->createToken(self::USER_ID);
        $this->repository->save($token);

        // WHEN: Token is deleted
        $this->repository->delete($token->id);

        // THEN: Token is no longer found
        $found = $this->repository->find($token->id);
        $this->assertNull($found);
    }

    #[Test]
    public function itInvalidatesEntireFamily(): void
    {
        // GIVEN: Multiple tokens in the same family
        $token1 = $this->createToken(self::USER_ID);
        $token2 = $this->rotateToken($token1);
        $token3 = $this->rotateToken($token2);

        $this->repository->save($token1);
        $this->repository->save($token2);
        $this->repository->save($token3);

        // WHEN: Family is invalidated
        $this->repository->invalidateFamily($token1->familyId);

        // THEN: All tokens in family are deleted
        $this->assertNull($this->repository->find($token1->id));
        $this->assertNull($this->repository->find($token2->id));
        $this->assertNull($this->repository->find($token3->id));
    }

    #[Test]
    public function itDoesNotAffectOtherFamilies(): void
    {
        // GIVEN: Tokens in different families
        $token1 = $this->createToken(self::USER_ID);
        $token2 = $this->createToken(self::USER_ID); // Different family (new login session)

        $this->repository->save($token1);
        $this->repository->save($token2);

        // WHEN: One family is invalidated
        $this->repository->invalidateFamily($token1->familyId);

        // THEN: Other family is intact
        $this->assertNull($this->repository->find($token1->id));
        $this->assertNotNull($this->repository->find($token2->id));
    }

    #[Test]
    public function itInvalidatesAllFamiliesForUser(): void
    {
        // GIVEN: Multiple families for the same user (multiple devices)
        $token1 = $this->createToken(self::USER_ID);
        $token2 = $this->createToken(self::USER_ID);
        $token3 = $this->createToken(self::USER_ID);

        $this->repository->save($token1);
        $this->repository->save($token2);
        $this->repository->save($token3);

        // All belong to same user but different families
        $userId = UserId::fromString(self::USER_ID);

        // Verify user has active sessions
        $this->assertTrue($this->repository->hasActiveSessionsForUser($userId));

        // WHEN: All tokens for user are invalidated
        $this->repository->invalidateAllForUser($userId);

        // THEN: No sessions remain for user
        $this->assertFalse($this->repository->hasActiveSessionsForUser($userId));
        $this->assertNull($this->repository->find($token1->id));
        $this->assertNull($this->repository->find($token2->id));
        $this->assertNull($this->repository->find($token3->id));
    }

    #[Test]
    public function itDoesNotAffectOtherUsers(): void
    {
        // GIVEN: Tokens for different users
        $token1 = $this->createToken(self::USER_ID);
        $token2 = $this->createToken(self::USER_ID_2);

        $this->repository->save($token1);
        $this->repository->save($token2);

        // WHEN: First user's tokens are invalidated
        $this->repository->invalidateAllForUser(UserId::fromString(self::USER_ID));

        // THEN: Second user's token is intact
        $this->assertNull($this->repository->find($token1->id));
        $this->assertNotNull($this->repository->find($token2->id));
    }

    #[Test]
    public function itTracksActiveSessionsForUser(): void
    {
        // GIVEN: No tokens for user
        $userId = UserId::fromString(self::USER_ID);
        $this->assertFalse($this->repository->hasActiveSessionsForUser($userId));

        // WHEN: Token is saved
        $token = $this->createToken(self::USER_ID);
        $this->repository->save($token);

        // THEN: User has active sessions
        $this->assertTrue($this->repository->hasActiveSessionsForUser($userId));
    }

    #[Test]
    public function itHandlesInvalidationOfNonExistentFamily(): void
    {
        // GIVEN: Non-existent family ID
        $familyId = TokenFamilyId::generate();

        // WHEN: Invalidating non-existent family
        $this->repository->invalidateFamily($familyId);

        // THEN: No exception thrown (idempotent operation)
        $this->assertTrue(true);
    }

    #[Test]
    public function itHandlesInvalidationOfNonExistentUser(): void
    {
        // GIVEN: Non-existent user ID
        $userId = UserId::fromString(self::USER_ID);

        // WHEN: Invalidating non-existent user's tokens
        $this->repository->invalidateAllForUser($userId);

        // THEN: No exception thrown (idempotent operation)
        $this->assertTrue(true);
    }

    #[Test]
    public function itHandlesDuplicateSavesIdempotently(): void
    {
        // GIVEN: A token
        $token = $this->createToken(self::USER_ID);

        // WHEN: Token is saved multiple times
        $this->repository->save($token);
        $this->repository->save($token);
        $this->repository->save($token);

        // THEN: Token exists once (no duplicates in indexes)
        $found = $this->repository->find($token->id);
        $this->assertNotNull($found);

        // Invalidating family should clean everything properly
        $this->repository->invalidateFamily($token->familyId);
        $this->assertNull($this->repository->find($token->id));
    }

    private function createToken(string $userId): RefreshToken
    {
        return RefreshToken::create(
            userId: UserId::fromString($userId),
            tenantId: TenantId::fromString(self::TENANT_ID),
            deviceFingerprint: DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'),
            issuedAt: $this->now,
        );
    }

    private function rotateToken(RefreshToken $token): RefreshToken
    {
        [$newToken, $oldToken] = $token->rotate($this->now->modify('+1 minute'));

        return $newToken;
    }
}
