<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Application\Service\Cache;

use App\Administration\Application\Dto\PaginatedResult;
use App\Administration\Application\Service\Cache\PaginatedQueryCache;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;

final class PaginatedQueryCacheTest extends TestCase
{
    private PaginatedQueryCache $cache;

    protected function setUp(): void
    {
        $this->cache = new PaginatedQueryCache(
            new TagAwareAdapter(new ArrayAdapter()),
        );
    }

    #[Test]
    public function loadsFromCallableOnCacheMiss(): void
    {
        $expected = new PaginatedResult(items: ['item1'], total: 1, page: 1, limit: 30);
        $callCount = 0;

        $result = $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], static function () use ($expected, &$callCount): PaginatedResult {
            ++$callCount;

            return $expected;
        });

        self::assertSame($expected, $result);
        self::assertSame(1, $callCount);
    }

    #[Test]
    public function returnsCachedResultOnHit(): void
    {
        $expected = new PaginatedResult(items: ['item1'], total: 1, page: 1, limit: 30);
        $callCount = 0;

        $loader = static function () use ($expected, &$callCount): PaginatedResult {
            ++$callCount;

            return $expected;
        };

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $loader);
        $result = $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $loader);

        self::assertSame(1, $callCount);
        self::assertEquals($expected, $result);
    }

    #[Test]
    public function differentParamsProduceDifferentCacheEntries(): void
    {
        $result1 = new PaginatedResult(items: ['page1'], total: 2, page: 1, limit: 1);
        $result2 = new PaginatedResult(items: ['page2'], total: 2, page: 2, limit: 1);

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], static fn (): PaginatedResult => $result1);
        $actual = $this->cache->getOrLoad('users', 'tenant-1', ['page' => 2], static fn (): PaginatedResult => $result2);

        self::assertEquals($result2, $actual);
    }

    #[Test]
    public function invalidatesCacheByEntityTypeAndTenant(): void
    {
        $callCount = 0;
        $loader = static function () use (&$callCount): PaginatedResult {
            ++$callCount;

            return new PaginatedResult(items: [], total: 0, page: 1, limit: 30);
        };

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $loader);
        self::assertSame(1, $callCount);

        $this->cache->invalidate('users', 'tenant-1');

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $loader);
        self::assertSame(2, $callCount);
    }

    #[Test]
    public function invalidationDoesNotAffectOtherEntityTypes(): void
    {
        $usersCallCount = 0;
        $classesCallCount = 0;

        $usersLoader = static function () use (&$usersCallCount): PaginatedResult {
            ++$usersCallCount;

            return new PaginatedResult(items: [], total: 0, page: 1, limit: 30);
        };

        $classesLoader = static function () use (&$classesCallCount): PaginatedResult {
            ++$classesCallCount;

            return new PaginatedResult(items: [], total: 0, page: 1, limit: 30);
        };

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $usersLoader);
        $this->cache->getOrLoad('classes', 'tenant-1', ['page' => 1], $classesLoader);

        $this->cache->invalidate('users', 'tenant-1');

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $usersLoader);
        $this->cache->getOrLoad('classes', 'tenant-1', ['page' => 1], $classesLoader);

        self::assertSame(2, $usersCallCount);
        self::assertSame(1, $classesCallCount);
    }

    #[Test]
    public function invalidationDoesNotAffectOtherTenants(): void
    {
        $tenant1Count = 0;
        $tenant2Count = 0;

        $tenant1Loader = static function () use (&$tenant1Count): PaginatedResult {
            ++$tenant1Count;

            return new PaginatedResult(items: [], total: 0, page: 1, limit: 30);
        };

        $tenant2Loader = static function () use (&$tenant2Count): PaginatedResult {
            ++$tenant2Count;

            return new PaginatedResult(items: [], total: 0, page: 1, limit: 30);
        };

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $tenant1Loader);
        $this->cache->getOrLoad('users', 'tenant-2', ['page' => 1], $tenant2Loader);

        $this->cache->invalidate('users', 'tenant-1');

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1], $tenant1Loader);
        $this->cache->getOrLoad('users', 'tenant-2', ['page' => 1], $tenant2Loader);

        self::assertSame(2, $tenant1Count);
        self::assertSame(1, $tenant2Count);
    }

    #[Test]
    public function paramOrderDoesNotAffectCacheKey(): void
    {
        $callCount = 0;
        $loader = static function () use (&$callCount): PaginatedResult {
            ++$callCount;

            return new PaginatedResult(items: [], total: 0, page: 1, limit: 30);
        };

        $this->cache->getOrLoad('users', 'tenant-1', ['page' => 1, 'role' => 'admin'], $loader);
        $this->cache->getOrLoad('users', 'tenant-1', ['role' => 'admin', 'page' => 1], $loader);

        self::assertSame(1, $callCount);
    }
}
