<?php

declare(strict_types=1);

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

use App\Administration\Domain\Exception\ParentInvitationNotFoundException;
use App\Administration\Domain\Model\Invitation\InvitationCode;
use App\Administration\Domain\Model\Invitation\InvitationStatus;
use App\Administration\Domain\Model\Invitation\ParentInvitation;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryParentInvitationRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class InMemoryParentInvitationRepositoryTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string OTHER_TENANT_ID = '550e8400-e29b-41d4-a716-446655440099';
    private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string CREATED_BY_ID = '550e8400-e29b-41d4-a716-446655440003';
    private const string CODE = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4';

    private InMemoryParentInvitationRepository $repository;

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

    #[Test]
    public function saveAndGetReturnsInvitation(): void
    {
        $invitation = $this->creerInvitation();
        $this->repository->save($invitation);

        $found = $this->repository->get($invitation->id, TenantId::fromString(self::TENANT_ID));

        self::assertTrue($found->id->equals($invitation->id));
    }

    #[Test]
    public function getThrowsExceptionWhenNotFound(): void
    {
        $this->expectException(ParentInvitationNotFoundException::class);

        $invitation = $this->creerInvitation();
        $this->repository->get($invitation->id, TenantId::fromString(self::TENANT_ID));
    }

    #[Test]
    public function getThrowsExceptionForWrongTenant(): void
    {
        $invitation = $this->creerInvitation();
        $this->repository->save($invitation);

        $this->expectException(ParentInvitationNotFoundException::class);

        $this->repository->get($invitation->id, TenantId::fromString(self::OTHER_TENANT_ID));
    }

    #[Test]
    public function findByIdReturnsNullWhenNotFound(): void
    {
        $invitation = $this->creerInvitation();

        self::assertNull($this->repository->findById($invitation->id, TenantId::fromString(self::TENANT_ID)));
    }

    #[Test]
    public function findByCodeReturnsInvitation(): void
    {
        $invitation = $this->creerInvitation();
        $this->repository->save($invitation);

        $found = $this->repository->findByCode(new InvitationCode(self::CODE));

        self::assertNotNull($found);
        self::assertTrue($found->id->equals($invitation->id));
    }

    #[Test]
    public function findByCodeReturnsNullWhenNotFound(): void
    {
        self::assertNull($this->repository->findByCode(new InvitationCode('11111111111111111111111111111111')));
    }

    #[Test]
    public function findAllByTenantReturnsOnlyMatchingTenant(): void
    {
        $invitation1 = $this->creerInvitation();
        $invitation2 = $this->creerInvitation(email: 'parent2@example.com', code: '22222222222222222222222222222222');
        $this->repository->save($invitation1);
        $this->repository->save($invitation2);

        $results = $this->repository->findAllByTenant(TenantId::fromString(self::TENANT_ID));

        self::assertCount(2, $results);
    }

    #[Test]
    public function findByStudentReturnsInvitationsForStudent(): void
    {
        $invitation = $this->creerInvitation();
        $this->repository->save($invitation);

        $results = $this->repository->findByStudent(
            UserId::fromString(self::STUDENT_ID),
            TenantId::fromString(self::TENANT_ID),
        );

        self::assertCount(1, $results);
    }

    #[Test]
    public function findByStatusReturnsMatchingInvitations(): void
    {
        $invitation = $this->creerInvitation();
        $this->repository->save($invitation);

        $pending = $this->repository->findByStatus(InvitationStatus::PENDING, TenantId::fromString(self::TENANT_ID));
        $sent = $this->repository->findByStatus(InvitationStatus::SENT, TenantId::fromString(self::TENANT_ID));

        self::assertCount(1, $pending);
        self::assertCount(0, $sent);
    }

    #[Test]
    public function findExpiredSentReturnsOnlySentAndExpired(): void
    {
        $invitation = $this->creerInvitation(createdAt: new DateTimeImmutable('2026-01-01 10:00:00'));
        $invitation->envoyer(new DateTimeImmutable('2026-01-01 11:00:00'));
        $this->repository->save($invitation);

        $results = $this->repository->findExpiredSent(new DateTimeImmutable('2026-02-01 10:00:00'));

        self::assertCount(1, $results);
    }

    #[Test]
    public function findExpiredSentDoesNotReturnNonExpired(): void
    {
        $invitation = $this->creerInvitation();
        $invitation->envoyer(new DateTimeImmutable('2026-02-20 11:00:00'));
        $this->repository->save($invitation);

        $results = $this->repository->findExpiredSent(new DateTimeImmutable('2026-02-21 10:00:00'));

        self::assertCount(0, $results);
    }

    #[Test]
    public function deleteRemovesInvitation(): void
    {
        $invitation = $this->creerInvitation();
        $this->repository->save($invitation);

        $this->repository->delete($invitation->id, TenantId::fromString(self::TENANT_ID));

        self::assertNull($this->repository->findById($invitation->id, TenantId::fromString(self::TENANT_ID)));
        self::assertNull($this->repository->findByCode(new InvitationCode(self::CODE)));
    }

    #[Test]
    public function saveUpdatesCodeIndexOnResend(): void
    {
        $invitation = $this->creerInvitation();
        $invitation->envoyer(new DateTimeImmutable('2026-02-20 11:00:00'));
        $this->repository->save($invitation);

        $newCode = new InvitationCode('33333333333333333333333333333333');
        $invitation->renvoyer($newCode, new DateTimeImmutable('2026-02-25 10:00:00'));
        $this->repository->save($invitation);

        self::assertNull($this->repository->findByCode(new InvitationCode(self::CODE)));
        self::assertNotNull($this->repository->findByCode($newCode));
    }

    private function creerInvitation(
        string $email = 'parent@example.com',
        string $code = self::CODE,
        ?DateTimeImmutable $createdAt = null,
    ): ParentInvitation {
        return ParentInvitation::creer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            studentId: UserId::fromString(self::STUDENT_ID),
            parentEmail: new Email($email),
            code: new InvitationCode($code),
            createdAt: $createdAt ?? new DateTimeImmutable('2026-02-20 10:00:00'),
            createdBy: UserId::fromString(self::CREATED_BY_ID),
        );
    }
}
