<?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 App\Administration\Domain\Event\AffectationRetiree;
use App\Administration\Domain\Event\ClasseCreee;
use App\Administration\Domain\Event\DroitImageModifie;
use App\Administration\Domain\Event\EleveInscrit;
use App\Administration\Domain\Event\EnseignantAffecte;
use App\Administration\Domain\Event\ImportElevesTermine;
use App\Administration\Domain\Event\ImportEnseignantsTermine;
use App\Administration\Domain\Event\InvitationParentActivee;
use App\Administration\Domain\Event\InvitationRenvoyee;
use App\Administration\Domain\Event\MatiereCreee;
use App\Administration\Domain\Event\UtilisateurInvite;
use App\Administration\Domain\Model\Import\ImportBatchId;
use App\Administration\Domain\Model\Invitation\ParentInvitationId;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\SchoolClass\ClassName;
use App\Administration\Domain\Model\Subject\SubjectCode;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\Subject\SubjectName;
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
use App\Administration\Domain\Model\User\ImageRightsStatus;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\EventHandler\PaginatedQueryCacheInvalidator;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;

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

    private PaginatedQueryCache $cache;
    private PaginatedQueryCacheInvalidator $invalidator;

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

    // === Users ===

    #[Test]
    public function utilisateurInviteInvalidatesUsersCache(): void
    {
        $this->warmCache('users', self::TENANT_ID);

        $this->invalidator->onUtilisateurInvite($this->createUtilisateurInvite(self::TENANT_ID));

        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
    }

    #[Test]
    public function invitationRenvoyeeInvalidatesUsersCache(): void
    {
        $this->warmCache('users', self::TENANT_ID);

        $event = new InvitationRenvoyee(
            userId: UserId::generate(),
            email: 'test@example.com',
            tenantId: TenantId::fromString(self::TENANT_ID),
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onInvitationRenvoyee($event);

        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
    }

    #[Test]
    public function eleveInscritInvalidatesUsersAndImageRightsCache(): void
    {
        $this->warmCache('users', self::TENANT_ID);
        $this->warmCache('students_image_rights', self::TENANT_ID);

        $event = new EleveInscrit(
            userId: UserId::generate(),
            firstName: 'Alice',
            lastName: 'Martin',
            tenantId: TenantId::fromString(self::TENANT_ID),
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onEleveInscrit($event);

        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
        $this->assertCacheWasInvalidated('students_image_rights', self::TENANT_ID);
    }

    // === Classes ===

    #[Test]
    public function classeCreeeInvalidatesClassesAndAssignmentsCache(): void
    {
        $this->warmCache('classes', self::TENANT_ID);
        $this->warmCache('assignments', self::TENANT_ID);

        $event = new ClasseCreee(
            classId: ClassId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            name: new ClassName('6ème A'),
            level: null,
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onClasseCreee($event);

        $this->assertCacheWasInvalidated('classes', self::TENANT_ID);
        $this->assertCacheWasInvalidated('assignments', self::TENANT_ID);
    }

    // === Subjects ===

    #[Test]
    public function matiereCreeeInvalidatesSubjectsAndAssignmentsCache(): void
    {
        $this->warmCache('subjects', self::TENANT_ID);
        $this->warmCache('assignments', self::TENANT_ID);

        $event = new MatiereCreee(
            subjectId: SubjectId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            name: new SubjectName('Mathématiques'),
            code: new SubjectCode('MATH'),
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onMatiereCreee($event);

        $this->assertCacheWasInvalidated('subjects', self::TENANT_ID);
        $this->assertCacheWasInvalidated('assignments', self::TENANT_ID);
    }

    // === Assignments ===

    #[Test]
    public function enseignantAffecteInvalidatesAssignmentsCache(): void
    {
        $this->warmCache('assignments', self::TENANT_ID);

        $event = new EnseignantAffecte(
            assignmentId: TeacherAssignmentId::generate(),
            teacherId: UserId::generate(),
            classId: ClassId::generate(),
            subjectId: SubjectId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onEnseignantAffecte($event);

        $this->assertCacheWasInvalidated('assignments', self::TENANT_ID);
    }

    #[Test]
    public function affectationRetireeInvalidatesAssignmentsCache(): void
    {
        $this->warmCache('assignments', self::TENANT_ID);

        $event = new AffectationRetiree(
            assignmentId: TeacherAssignmentId::generate(),
            teacherId: UserId::generate(),
            classId: ClassId::generate(),
            subjectId: SubjectId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onAffectationRetiree($event);

        $this->assertCacheWasInvalidated('assignments', self::TENANT_ID);
    }

    // === Parent invitations ===

    #[Test]
    public function invitationParentActiveeInvalidatesParentInvitationsAndUsersCache(): void
    {
        $this->warmCache('parent_invitations', self::TENANT_ID);
        $this->warmCache('users', self::TENANT_ID);

        $event = new InvitationParentActivee(
            invitationId: ParentInvitationId::generate(),
            studentId: UserId::generate(),
            parentUserId: UserId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onInvitationParentActivee($event);

        $this->assertCacheWasInvalidated('parent_invitations', self::TENANT_ID);
        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
    }

    // === Image rights ===

    #[Test]
    public function droitImageModifieInvalidatesImageRightsCache(): void
    {
        $this->warmCache('students_image_rights', self::TENANT_ID);

        $event = new DroitImageModifie(
            userId: UserId::generate(),
            email: 'alice@example.com',
            ancienStatut: ImageRightsStatus::NOT_SPECIFIED,
            nouveauStatut: ImageRightsStatus::AUTHORIZED,
            modifiePar: UserId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onDroitImageModifie($event);

        $this->assertCacheWasInvalidated('students_image_rights', self::TENANT_ID);
    }

    // === Imports ===

    #[Test]
    public function importElevesTermineInvalidatesUsersImageRightsAndClassesCache(): void
    {
        $this->warmCache('users', self::TENANT_ID);
        $this->warmCache('students_image_rights', self::TENANT_ID);
        $this->warmCache('classes', self::TENANT_ID);

        $event = new ImportElevesTermine(
            batchId: ImportBatchId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            importedCount: 10,
            errorCount: 0,
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onImportElevesTermine($event);

        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
        $this->assertCacheWasInvalidated('students_image_rights', self::TENANT_ID);
        $this->assertCacheWasInvalidated('classes', self::TENANT_ID);
    }

    #[Test]
    public function importEnseignantsTermineInvalidatesUsersAndAssignmentsCache(): void
    {
        $this->warmCache('users', self::TENANT_ID);
        $this->warmCache('assignments', self::TENANT_ID);

        $event = new ImportEnseignantsTermine(
            batchId: ImportBatchId::generate(),
            tenantId: TenantId::fromString(self::TENANT_ID),
            importedCount: 5,
            errorCount: 0,
            occurredOn: new DateTimeImmutable(),
        );

        $this->invalidator->onImportEnseignantsTermine($event);

        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
        $this->assertCacheWasInvalidated('assignments', self::TENANT_ID);
    }

    // === Tenant isolation ===

    #[Test]
    public function invalidationDoesNotAffectOtherTenants(): void
    {
        $otherTenantId = '550e8400-e29b-41d4-a716-446655440099';

        $this->warmCache('users', self::TENANT_ID);
        $this->warmCache('users', $otherTenantId);

        $this->invalidator->onUtilisateurInvite($this->createUtilisateurInvite(self::TENANT_ID));

        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
        $this->assertCacheStillValid('users', $otherTenantId);
    }

    #[Test]
    public function invalidationDoesNotAffectOtherEntityTypes(): void
    {
        $this->warmCache('users', self::TENANT_ID);
        $this->warmCache('classes', self::TENANT_ID);

        $this->invalidator->onUtilisateurInvite($this->createUtilisateurInvite(self::TENANT_ID));

        $this->assertCacheWasInvalidated('users', self::TENANT_ID);
        $this->assertCacheStillValid('classes', self::TENANT_ID);
    }

    private function warmCache(string $entityType, string $tenantId): void
    {
        $this->cache->getOrLoad(
            $entityType,
            $tenantId,
            ['page' => 1, 'limit' => 30],
            static fn (): PaginatedResult => new PaginatedResult(items: ['original'], total: 1, page: 1, limit: 30),
        );
    }

    private function assertCacheWasInvalidated(string $entityType, string $tenantId): void
    {
        $result = $this->cache->getOrLoad(
            $entityType,
            $tenantId,
            ['page' => 1, 'limit' => 30],
            static fn (): PaginatedResult => new PaginatedResult(items: ['fresh'], total: 1, page: 1, limit: 30),
        );

        self::assertSame(['fresh'], $result->items, "Cache for {$entityType}/{$tenantId} should have been invalidated");
    }

    private function assertCacheStillValid(string $entityType, string $tenantId): void
    {
        $result = $this->cache->getOrLoad(
            $entityType,
            $tenantId,
            ['page' => 1, 'limit' => 30],
            static fn (): PaginatedResult => new PaginatedResult(items: ['fresh'], total: 1, page: 1, limit: 30),
        );

        self::assertSame(['original'], $result->items, "Cache for {$entityType}/{$tenantId} should still contain original data");
    }

    private function createUtilisateurInvite(string $tenantId): UtilisateurInvite
    {
        return new UtilisateurInvite(
            userId: UserId::generate(),
            email: 'test@example.com',
            role: 'ROLE_PROF',
            firstName: 'Jean',
            lastName: 'Dupont',
            tenantId: TenantId::fromString($tenantId),
            occurredOn: new DateTimeImmutable(),
        );
    }
}
