<?php

declare(strict_types=1);

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

use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Infrastructure\Persistence\Cache\CacheUserRepository;
use App\Shared\Infrastructure\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;

/**
 * Tests for CacheUserRepository.
 *
 * Key invariants:
 * - Users must not expire from cache (unlike activation tokens which have 7-day TTL)
 * - Email lookups are scoped by tenant ID for multi-tenant isolation
 */
final class CacheUserRepositoryTest extends TestCase
{
    private const string TENANT_ALPHA_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
    private const string TENANT_BETA_ID = 'b2c3d4e5-f6a7-8901-bcde-f12345678901';

    #[Test]
    public function userIsSavedWithoutExpiration(): void
    {
        // Arrange: Create a mock cache that tracks expiration settings
        $expirationSet = null;

        $cacheItem = $this->createMock(CacheItemInterface::class);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')
            ->willReturnCallback(static function ($ttl) use (&$expirationSet, $cacheItem) {
                $expirationSet = $ttl;

                return $cacheItem;
            });

        $cachePool = $this->createMock(CacheItemPoolInterface::class);
        $cachePool->method('getItem')->willReturn($cacheItem);
        $cachePool->method('save')->willReturn(true);

        $repository = new CacheUserRepository($cachePool);

        $user = User::creer(
            email: new Email('test@example.com'),
            role: Role::PARENT,
            tenantId: TenantId::fromString(self::TENANT_ALPHA_ID),
            schoolName: 'École Test',
            dateNaissance: null,
            createdAt: new DateTimeImmutable(),
        );

        // Act
        $repository->save($user);

        // Assert: No expiration should be set (expiresAfter should not be called with a TTL)
        // The users.cache pool is configured with default_lifetime: 0 (no expiration)
        // But CacheUserRepository should NOT explicitly set any TTL
        self::assertNull(
            $expirationSet,
            'User cache entries should not have explicit expiration set by the repository'
        );
    }

    #[Test]
    public function userCanBeRetrievedById(): void
    {
        // Arrange
        $userId = '550e8400-e29b-41d4-a716-446655440001';
        $email = 'test@example.com';

        $userData = [
            'id' => $userId,
            'email' => $email,
            'role' => 'ROLE_PARENT',
            'tenant_id' => self::TENANT_ALPHA_ID,
            'school_name' => 'École Test',
            'statut' => 'pending',
            'hashed_password' => null,
            'date_naissance' => null,
            'created_at' => '2026-01-15T10:00:00+00:00',
            'activated_at' => null,
            'consentement_parental' => null,
        ];

        $cacheItem = $this->createMock(CacheItemInterface::class);
        $cacheItem->method('isHit')->willReturn(true);
        $cacheItem->method('get')->willReturn($userData);

        $cachePool = $this->createMock(CacheItemPoolInterface::class);
        $cachePool->method('getItem')->willReturn($cacheItem);

        $repository = new CacheUserRepository($cachePool);

        // Act
        $user = $repository->findById(\App\Administration\Domain\Model\User\UserId::fromString($userId));

        // Assert
        self::assertNotNull($user);
        self::assertSame($userId, (string) $user->id);
        self::assertSame($email, (string) $user->email);
        self::assertSame(Role::PARENT, $user->role);
    }

    #[Test]
    public function userCanBeRetrievedByEmailWithinSameTenant(): void
    {
        // Arrange
        $userId = '550e8400-e29b-41d4-a716-446655440001';
        $email = 'test@example.com';
        $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID);

        $userData = [
            'id' => $userId,
            'email' => $email,
            'role' => 'ROLE_PARENT',
            'tenant_id' => self::TENANT_ALPHA_ID,
            'school_name' => 'École Test',
            'statut' => 'pending',
            'hashed_password' => null,
            'date_naissance' => null,
            'created_at' => '2026-01-15T10:00:00+00:00',
            'activated_at' => null,
            'consentement_parental' => null,
        ];

        $emailIndexItem = $this->createMock(CacheItemInterface::class);
        $emailIndexItem->method('isHit')->willReturn(true);
        $emailIndexItem->method('get')->willReturn($userId);

        $userItem = $this->createMock(CacheItemInterface::class);
        $userItem->method('isHit')->willReturn(true);
        $userItem->method('get')->willReturn($userData);

        $cachePool = $this->createMock(CacheItemPoolInterface::class);
        $cachePool->method('getItem')
            ->willReturnCallback(static function ($key) use ($emailIndexItem, $userItem, $tenantId) {
                // Email index key should include tenant ID
                $expectedEmailKey = 'user_email:' . $tenantId . ':test_at_example_dot_com';
                if ($key === $expectedEmailKey) {
                    return $emailIndexItem;
                }

                return $userItem;
            });

        $repository = new CacheUserRepository($cachePool);

        // Act
        $user = $repository->findByEmail(new Email($email), $tenantId);

        // Assert
        self::assertNotNull($user);
        self::assertSame($userId, (string) $user->id);
        self::assertSame($email, (string) $user->email);
    }

    #[Test]
    public function userCannotBeFoundByEmailInDifferentTenant(): void
    {
        // Arrange: User exists in tenant Alpha
        $tenantAlpha = TenantId::fromString(self::TENANT_ALPHA_ID);
        $tenantBeta = TenantId::fromString(self::TENANT_BETA_ID);
        $email = new Email('test@example.com');

        // Cache miss for tenant Beta's email index
        $missItem = $this->createMock(CacheItemInterface::class);
        $missItem->method('isHit')->willReturn(false);

        $cachePool = $this->createMock(CacheItemPoolInterface::class);
        $cachePool->method('getItem')
            ->willReturnCallback(static function ($key) use ($missItem, $tenantBeta) {
                // When looking up with tenant Beta, return cache miss
                $betaEmailKey = 'user_email:' . $tenantBeta . ':test_at_example_dot_com';
                if ($key === $betaEmailKey) {
                    return $missItem;
                }

                // For any other key, also return miss
                return $missItem;
            });

        $repository = new CacheUserRepository($cachePool);

        // Act: Try to find user in tenant Beta (where they don't exist)
        $user = $repository->findByEmail($email, $tenantBeta);

        // Assert: User should not be found
        self::assertNull($user, 'User from tenant Alpha should not be found when searching in tenant Beta');
    }

    #[Test]
    public function emailIndexKeyIncludesTenantId(): void
    {
        // Arrange: Track what cache keys are used
        $savedKeys = [];

        $cacheItem = $this->createMock(CacheItemInterface::class);
        $cacheItem->method('set')->willReturnSelf();

        $cachePool = $this->createMock(CacheItemPoolInterface::class);
        $cachePool->method('getItem')
            ->willReturnCallback(static function ($key) use (&$savedKeys, $cacheItem) {
                $savedKeys[] = $key;

                return $cacheItem;
            });
        $cachePool->method('save')->willReturn(true);

        $repository = new CacheUserRepository($cachePool);

        $user = User::creer(
            email: new Email('test@example.com'),
            role: Role::PARENT,
            tenantId: TenantId::fromString(self::TENANT_ALPHA_ID),
            schoolName: 'École Test',
            dateNaissance: null,
            createdAt: new DateTimeImmutable(),
        );

        // Act
        $repository->save($user);

        // Assert: Email index key should include tenant ID
        $emailIndexKey = 'user_email:' . self::TENANT_ALPHA_ID . ':test_at_example_dot_com';
        self::assertContains(
            $emailIndexKey,
            $savedKeys,
            'Email index cache key should include tenant ID for multi-tenant isolation'
        );
    }
}
