<?php

declare(strict_types=1);

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

use App\Administration\Domain\Event\InvitationRenvoyee;
use App\Administration\Domain\Event\UtilisateurBloque;
use App\Administration\Domain\Event\UtilisateurDebloque;
use App\Administration\Domain\Event\UtilisateurInvite;
use App\Administration\Domain\Exception\UtilisateurDejaInviteException;
use App\Administration\Domain\Exception\UtilisateurNonBlocableException;
use App\Administration\Domain\Exception\UtilisateurNonDeblocableException;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\StatutCompte;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class UserInvitationTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string SCHOOL_NAME = 'École Alpha';

    private Clock $clock;

    protected function setUp(): void
    {
        $this->clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-07 10:00:00');
            }
        };
    }

    #[Test]
    public function inviterCreatesUserWithPendingStatusAndRecordsInvitedAt(): void
    {
        $invitedAt = new DateTimeImmutable('2026-02-07 10:00:00');

        $user = User::inviter(
            email: new Email('teacher@example.com'),
            role: Role::PROF,
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: self::SCHOOL_NAME,
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: $invitedAt,
        );

        self::assertSame(StatutCompte::EN_ATTENTE, $user->statut);
        self::assertSame('Jean', $user->firstName);
        self::assertSame('Dupont', $user->lastName);
        self::assertEquals($invitedAt, $user->invitedAt);
        self::assertNull($user->hashedPassword);
        self::assertNull($user->activatedAt);
        self::assertNull($user->blockedAt);
        self::assertNull($user->blockedReason);
    }

    #[Test]
    public function inviterRecordsUtilisateurInviteEvent(): void
    {
        $user = $this->inviteUser();

        $events = $user->pullDomainEvents();

        self::assertCount(1, $events);
        self::assertInstanceOf(UtilisateurInvite::class, $events[0]);
    }

    #[Test]
    public function renvoyerInvitationUpdatesInvitedAtAndRecordsEvent(): void
    {
        $user = $this->inviteUser();
        $user->pullDomainEvents();

        $newInvitedAt = new DateTimeImmutable('2026-02-14 10:00:00');
        $user->renvoyerInvitation($newInvitedAt);

        self::assertEquals($newInvitedAt, $user->invitedAt);

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

    #[Test]
    public function renvoyerInvitationThrowsWhenUserIsActive(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );

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

        $user->renvoyerInvitation(new DateTimeImmutable('2026-02-14 10:00:00'));
    }

    #[Test]
    public function bloquerSetsStatusToSuspenduWithReasonAndDate(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );
        $user->pullDomainEvents();

        $blockedAt = new DateTimeImmutable('2026-02-10 15:00:00');
        $user->bloquer('Comportement inapproprié', $blockedAt);

        self::assertSame(StatutCompte::SUSPENDU, $user->statut);
        self::assertEquals($blockedAt, $user->blockedAt);
        self::assertSame('Comportement inapproprié', $user->blockedReason);
        self::assertFalse($user->peutSeConnecter());
    }

    #[Test]
    public function bloquerRecordsUtilisateurBloqueEvent(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );
        $user->pullDomainEvents();

        $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00'));

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

    #[Test]
    public function bloquerThrowsWhenAlreadySuspendu(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );
        $user->bloquer('Première raison', new DateTimeImmutable('2026-02-10 15:00:00'));

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

        $user->bloquer('Seconde raison', new DateTimeImmutable('2026-02-11 15:00:00'));
    }

    #[Test]
    public function bloquerThrowsWhenEnAttente(): void
    {
        $user = $this->inviteUser();

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

        $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00'));
    }

    #[Test]
    public function bloquerThrowsWhenConsentementRequis(): void
    {
        $user = User::reconstitute(
            id: UserId::generate(),
            email: new Email('minor@example.com'),
            roles: [Role::ELEVE],
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: self::SCHOOL_NAME,
            statut: StatutCompte::CONSENTEMENT_REQUIS,
            dateNaissance: new DateTimeImmutable('2015-01-01'),
            createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
            hashedPassword: null,
            activatedAt: null,
            consentementParental: null,
        );

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

        $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00'));
    }

    #[Test]
    public function debloquerRestoresActiveStatusAndClearsBlockedInfo(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );
        $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00'));
        $user->pullDomainEvents();

        $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00'));

        self::assertSame(StatutCompte::ACTIF, $user->statut);
        self::assertNull($user->blockedAt);
        self::assertNull($user->blockedReason);
    }

    #[Test]
    public function debloquerRecordsUtilisateurDebloqueEvent(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );
        $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00'));
        $user->pullDomainEvents();

        $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00'));

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

    #[Test]
    public function debloquerThrowsWhenNotSuspendu(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );

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

        $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00'));
    }

    #[Test]
    public function debloquerThrowsWhenEnAttente(): void
    {
        $user = $this->inviteUser();

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

        $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00'));
    }

    #[Test]
    public function estInvitationExpireeReturnsTrueAfter7Days(): void
    {
        $invitedAt = new DateTimeImmutable('2026-01-30 10:00:00');
        $user = User::inviter(
            email: new Email('teacher@example.com'),
            role: Role::PROF,
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: self::SCHOOL_NAME,
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: $invitedAt,
        );

        // 8 jours après
        $checkAt = new DateTimeImmutable('2026-02-07 10:00:01');
        self::assertTrue($user->estInvitationExpiree($checkAt));
    }

    #[Test]
    public function estInvitationExpireeReturnsFalseWithin7Days(): void
    {
        $invitedAt = new DateTimeImmutable('2026-02-05 10:00:00');
        $user = User::inviter(
            email: new Email('teacher@example.com'),
            role: Role::PROF,
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: self::SCHOOL_NAME,
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: $invitedAt,
        );

        // 2 jours après
        $checkAt = new DateTimeImmutable('2026-02-07 10:00:00');
        self::assertFalse($user->estInvitationExpiree($checkAt));
    }

    #[Test]
    public function estInvitationExpireeReturnsFalseForActiveUser(): void
    {
        $user = $this->inviteUser();
        $user->activer(
            '$argon2id$hashed',
            new DateTimeImmutable(),
            new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
        );

        // Même longtemps après, un utilisateur actif n'a pas d'invitation expirée
        $checkAt = new DateTimeImmutable('2027-01-01 10:00:00');
        self::assertFalse($user->estInvitationExpiree($checkAt));
    }

    private function inviteUser(): User
    {
        return User::inviter(
            email: new Email('teacher@example.com'),
            role: Role::PROF,
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: self::SCHOOL_NAME,
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: new DateTimeImmutable('2026-02-07 10:00:00'),
        );
    }
}
