<?php

declare(strict_types=1);

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

use App\Administration\Domain\Model\RefreshToken\TokenFamilyId;
use App\Administration\Domain\Model\Session\DeviceInfo;
use App\Administration\Domain\Model\Session\Location;
use App\Administration\Domain\Model\Session\Session;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Persistence\Redis\RedisSessionRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

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

    private ArrayAdapter $cache;
    private RedisSessionRepository $repository;

    protected function setUp(): void
    {
        $this->cache = new ArrayAdapter();
        $this->repository = new RedisSessionRepository($this->cache);
    }

    #[Test]
    public function savePersistsSession(): void
    {
        $session = $this->createSession();

        $this->repository->save($session, 86400);

        $retrieved = $this->repository->findByFamilyId($session->familyId);
        self::assertNotNull($retrieved);
        self::assertTrue($session->familyId->equals($retrieved->familyId));
    }

    #[Test]
    public function findByFamilyIdReturnsNullWhenNotFound(): void
    {
        $nonExistentId = TokenFamilyId::generate();

        $result = $this->repository->findByFamilyId($nonExistentId);

        self::assertNull($result);
    }

    #[Test]
    public function findAllByUserIdReturnsAllUserSessions(): void
    {
        $userId = UserId::generate();
        $tenantId = TenantId::fromString(self::TENANT_ID);

        $session1 = Session::create(
            TokenFamilyId::generate(),
            $userId,
            $tenantId,
            DeviceInfo::fromUserAgent('Chrome'),
            Location::unknown(),
            new DateTimeImmutable(),
        );
        $session2 = Session::create(
            TokenFamilyId::generate(),
            $userId,
            $tenantId,
            DeviceInfo::fromUserAgent('Firefox'),
            Location::unknown(),
            new DateTimeImmutable(),
        );
        $otherUserSession = Session::create(
            TokenFamilyId::generate(),
            UserId::generate(),
            $tenantId,
            DeviceInfo::fromUserAgent('Safari'),
            Location::unknown(),
            new DateTimeImmutable(),
        );

        $this->repository->save($session1, 86400);
        $this->repository->save($session2, 86400);
        $this->repository->save($otherUserSession, 86400);

        $sessions = $this->repository->findAllByUserId($userId);

        self::assertCount(2, $sessions);

        $familyIds = array_map(
            static fn (Session $s) => (string) $s->familyId,
            $sessions,
        );
        self::assertContains((string) $session1->familyId, $familyIds);
        self::assertContains((string) $session2->familyId, $familyIds);
    }

    #[Test]
    public function deleteRemovesSession(): void
    {
        $session = $this->createSession();
        $this->repository->save($session, 86400);

        $this->repository->delete($session->familyId);

        self::assertNull($this->repository->findByFamilyId($session->familyId));
    }

    #[Test]
    public function deleteRemovesSessionFromUserIndex(): void
    {
        $userId = UserId::generate();
        $session = Session::create(
            TokenFamilyId::generate(),
            $userId,
            TenantId::fromString(self::TENANT_ID),
            DeviceInfo::fromUserAgent('Chrome'),
            Location::unknown(),
            new DateTimeImmutable(),
        );

        $this->repository->save($session, 86400);
        $this->repository->delete($session->familyId);

        $sessions = $this->repository->findAllByUserId($userId);
        self::assertCount(0, $sessions);
    }

    #[Test]
    public function deleteAllExceptRemovesOtherSessions(): void
    {
        $userId = UserId::generate();
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $currentFamilyId = TokenFamilyId::generate();

        $currentSession = Session::create(
            $currentFamilyId,
            $userId,
            $tenantId,
            DeviceInfo::fromUserAgent('Chrome'),
            Location::unknown(),
            new DateTimeImmutable(),
        );
        $otherSession1 = Session::create(
            TokenFamilyId::generate(),
            $userId,
            $tenantId,
            DeviceInfo::fromUserAgent('Firefox'),
            Location::unknown(),
            new DateTimeImmutable(),
        );
        $otherSession2 = Session::create(
            TokenFamilyId::generate(),
            $userId,
            $tenantId,
            DeviceInfo::fromUserAgent('Safari'),
            Location::unknown(),
            new DateTimeImmutable(),
        );

        $this->repository->save($currentSession, 86400);
        $this->repository->save($otherSession1, 86400);
        $this->repository->save($otherSession2, 86400);

        $deletedFamilyIds = $this->repository->deleteAllExcept($userId, $currentFamilyId);

        self::assertCount(2, $deletedFamilyIds);
        self::assertContains((string) $otherSession1->familyId, array_map(static fn ($id) => (string) $id, $deletedFamilyIds));
        self::assertContains((string) $otherSession2->familyId, array_map(static fn ($id) => (string) $id, $deletedFamilyIds));

        $sessions = $this->repository->findAllByUserId($userId);
        self::assertCount(1, $sessions);
        self::assertTrue($sessions[0]->familyId->equals($currentFamilyId));
    }

    #[Test]
    public function updateActivityUpdatesLastActivityTimestamp(): void
    {
        $session = $this->createSession();
        $this->repository->save($session, 86400);

        $newActivityAt = new DateTimeImmutable('2026-02-01 15:30:00');
        $remainingTtl = 43200; // 12 hours remaining
        $this->repository->updateActivity($session->familyId, $newActivityAt, $remainingTtl);

        $updated = $this->repository->findByFamilyId($session->familyId);
        self::assertNotNull($updated);
        self::assertEquals($newActivityAt, $updated->lastActivityAt);
    }

    #[Test]
    public function savePreservesAllSessionData(): void
    {
        $familyId = TokenFamilyId::generate();
        $userId = UserId::generate();
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $deviceInfo = DeviceInfo::fromUserAgent(
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
        );
        $location = Location::fromIp('8.8.8.8', 'United States', 'Mountain View');
        $createdAt = new DateTimeImmutable('2026-01-31 10:00:00');

        $session = Session::create(
            $familyId,
            $userId,
            $tenantId,
            $deviceInfo,
            $location,
            $createdAt,
        );

        $this->repository->save($session, 86400);
        $retrieved = $this->repository->findByFamilyId($familyId);

        self::assertNotNull($retrieved);
        self::assertTrue($familyId->equals($retrieved->familyId));
        self::assertTrue($userId->equals($retrieved->userId));
        self::assertTrue($tenantId->equals($retrieved->tenantId));
        self::assertSame('Desktop', $retrieved->deviceInfo->device);
        self::assertSame('Chrome 120', $retrieved->deviceInfo->browser);
        self::assertSame('Windows 10', $retrieved->deviceInfo->os);
        self::assertSame('United States', $retrieved->location->country);
        self::assertSame('Mountain View', $retrieved->location->city);
        self::assertEquals($createdAt, $retrieved->createdAt);
    }

    private function createSession(): Session
    {
        return Session::create(
            TokenFamilyId::generate(),
            UserId::generate(),
            TenantId::fromString(self::TENANT_ID),
            DeviceInfo::fromUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'),
            Location::fromIp('192.168.1.1', 'France', 'Paris'),
            new DateTimeImmutable('2026-01-31 10:00:00'),
        );
    }
}
