<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Domain\Model\Invitation;

use App\Administration\Domain\Event\InvitationParentActivee;
use App\Administration\Domain\Event\InvitationParentEnvoyee;
use App\Administration\Domain\Exception\InvitationDejaActiveeException;
use App\Administration\Domain\Exception\InvitationExpireeException;
use App\Administration\Domain\Exception\InvitationNonEnvoyeeException;
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\Invitation\ParentInvitationId;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\UserId;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class ParentInvitationTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string CREATED_BY_ID = '550e8400-e29b-41d4-a716-446655440003';
    private const string PARENT_EMAIL = 'parent@example.com';
    private const string CODE = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4';

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

        self::assertInstanceOf(ParentInvitationId::class, $invitation->id);
        self::assertTrue(TenantId::fromString(self::TENANT_ID)->equals($invitation->tenantId));
        self::assertTrue(UserId::fromString(self::STUDENT_ID)->equals($invitation->studentId));
        self::assertSame(self::PARENT_EMAIL, (string) $invitation->parentEmail);
        self::assertSame(self::CODE, (string) $invitation->code);
        self::assertSame(InvitationStatus::PENDING, $invitation->status);
        self::assertNull($invitation->sentAt);
        self::assertNull($invitation->activatedAt);
        self::assertNull($invitation->activatedUserId);
    }

    #[Test]
    public function creerSetsExpirationTo7DaysAfterCreation(): void
    {
        $createdAt = new DateTimeImmutable('2026-02-20 10:00:00');
        $expectedExpiration = new DateTimeImmutable('2026-02-27 10:00:00');

        $invitation = $this->creerInvitation(createdAt: $createdAt);

        self::assertEquals($expectedExpiration, $invitation->expiresAt);
    }

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

        $invitation->envoyer($sentAt);

        self::assertSame(InvitationStatus::SENT, $invitation->status);
        self::assertEquals($sentAt, $invitation->sentAt);
    }

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

        $invitation->envoyer($sentAt);

        $events = $invitation->pullDomainEvents();
        self::assertCount(1, $events);
        self::assertInstanceOf(InvitationParentEnvoyee::class, $events[0]);
    }

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

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

        $invitation->envoyer(new DateTimeImmutable('2026-02-21 10:00:00'));
    }

    #[Test]
    public function activerChangesStatusToActivated(): void
    {
        $invitation = $this->creerInvitationEnvoyee();
        $parentUserId = UserId::generate();
        $activatedAt = new DateTimeImmutable('2026-02-21 10:00:00');

        $invitation->activer($parentUserId, $activatedAt);

        self::assertSame(InvitationStatus::ACTIVATED, $invitation->status);
        self::assertEquals($activatedAt, $invitation->activatedAt);
        self::assertTrue($parentUserId->equals($invitation->activatedUserId));
    }

    #[Test]
    public function activerRecordsInvitationParentActiveeEvent(): void
    {
        $invitation = $this->creerInvitationEnvoyee();
        $parentUserId = UserId::generate();
        $activatedAt = new DateTimeImmutable('2026-02-21 10:00:00');
        $invitation->pullDomainEvents();

        $invitation->activer($parentUserId, $activatedAt);

        $events = $invitation->pullDomainEvents();
        self::assertCount(1, $events);
        self::assertInstanceOf(InvitationParentActivee::class, $events[0]);
    }

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

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

        $invitation->activer(UserId::generate(), new DateTimeImmutable('2026-02-22 10:00:00'));
    }

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

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

        $invitation->activer(UserId::generate(), new DateTimeImmutable('2026-02-21 10:00:00'));
    }

    #[Test]
    public function activerThrowsExceptionWhenExpired(): void
    {
        $invitation = $this->creerInvitationEnvoyee(
            createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
        );

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

        $invitation->activer(
            UserId::generate(),
            new DateTimeImmutable('2026-02-20 10:00:00'),
        );
    }

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

        $invitation->marquerExpiree();

        self::assertSame(InvitationStatus::EXPIRED, $invitation->status);
    }

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

        $invitation->marquerExpiree();

        self::assertSame(InvitationStatus::PENDING, $invitation->status);
    }

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

        $invitation->marquerExpiree();

        self::assertSame(InvitationStatus::ACTIVATED, $invitation->status);
    }

    #[Test]
    public function renvoyerResetsCodeAndStatusAndExpiration(): void
    {
        $invitation = $this->creerInvitationEnvoyee(
            createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
        );
        $nouveauCode = new InvitationCode('11111111111111111111111111111111');
        $renvoyeAt = new DateTimeImmutable('2026-02-15 10:00:00');
        $expectedExpiration = new DateTimeImmutable('2026-02-22 10:00:00');

        $invitation->renvoyer($nouveauCode, $renvoyeAt);

        self::assertSame(InvitationStatus::SENT, $invitation->status);
        self::assertSame('11111111111111111111111111111111', (string) $invitation->code);
        self::assertEquals($renvoyeAt, $invitation->sentAt);
        self::assertEquals($expectedExpiration, $invitation->expiresAt);
    }

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

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

        $invitation->renvoyer(
            new InvitationCode('11111111111111111111111111111111'),
            new DateTimeImmutable('2026-02-21 10:00:00'),
        );
    }

    #[Test]
    public function renvoyerWorksOnExpiredInvitation(): void
    {
        $invitation = $this->creerInvitationEnvoyee(
            createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
        );
        $invitation->marquerExpiree();
        $nouveauCode = new InvitationCode('22222222222222222222222222222222');
        $renvoyeAt = new DateTimeImmutable('2026-02-15 10:00:00');

        $invitation->renvoyer($nouveauCode, $renvoyeAt);

        self::assertSame(InvitationStatus::SENT, $invitation->status);
    }

    #[Test]
    public function estExpireeReturnsFalseBeforeExpiration(): void
    {
        $invitation = $this->creerInvitation(
            createdAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );

        self::assertFalse($invitation->estExpiree(new DateTimeImmutable('2026-02-25 10:00:00')));
    }

    #[Test]
    public function estExpireeReturnsTrueAfterExpiration(): void
    {
        $invitation = $this->creerInvitation(
            createdAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );

        self::assertTrue($invitation->estExpiree(new DateTimeImmutable('2026-02-28 10:00:00')));
    }

    #[Test]
    public function estExpireeReturnsTrueAtExactExpiration(): void
    {
        $invitation = $this->creerInvitation(
            createdAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );

        self::assertTrue($invitation->estExpiree(new DateTimeImmutable('2026-02-27 10:00:00')));
    }

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

        self::assertFalse($invitation->estActivee());
    }

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

        self::assertTrue($invitation->estActivee());
    }

    #[Test]
    public function reconstitutePreservesAllProperties(): void
    {
        $id = ParentInvitationId::generate();
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $studentId = UserId::fromString(self::STUDENT_ID);
        $parentEmail = new Email(self::PARENT_EMAIL);
        $code = new InvitationCode(self::CODE);
        $createdAt = new DateTimeImmutable('2026-02-20 10:00:00');
        $createdBy = UserId::fromString(self::CREATED_BY_ID);
        $sentAt = new DateTimeImmutable('2026-02-20 11:00:00');
        $activatedAt = new DateTimeImmutable('2026-02-21 10:00:00');
        $activatedUserId = UserId::generate();

        $invitation = ParentInvitation::reconstitute(
            id: $id,
            tenantId: $tenantId,
            studentId: $studentId,
            parentEmail: $parentEmail,
            code: $code,
            status: InvitationStatus::ACTIVATED,
            expiresAt: new DateTimeImmutable('2026-02-27 10:00:00'),
            createdAt: $createdAt,
            createdBy: $createdBy,
            sentAt: $sentAt,
            activatedAt: $activatedAt,
            activatedUserId: $activatedUserId,
        );

        self::assertTrue($id->equals($invitation->id));
        self::assertTrue($tenantId->equals($invitation->tenantId));
        self::assertTrue($studentId->equals($invitation->studentId));
        self::assertTrue($parentEmail->equals($invitation->parentEmail));
        self::assertTrue($code->equals($invitation->code));
        self::assertSame(InvitationStatus::ACTIVATED, $invitation->status);
        self::assertEquals($sentAt, $invitation->sentAt);
        self::assertEquals($activatedAt, $invitation->activatedAt);
        self::assertTrue($activatedUserId->equals($invitation->activatedUserId));
    }

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

    private function creerInvitationEnvoyee(?DateTimeImmutable $createdAt = null): ParentInvitation
    {
        $invitation = $this->creerInvitation($createdAt);
        $invitation->envoyer($createdAt ?? new DateTimeImmutable('2026-02-20 10:30:00'));

        return $invitation;
    }

    private function creerInvitationActivee(): ParentInvitation
    {
        $invitation = $this->creerInvitationEnvoyee();
        $invitation->activer(
            UserId::generate(),
            new DateTimeImmutable('2026-02-21 10:00:00'),
        );

        return $invitation;
    }
}
