<?php

declare(strict_types=1);

namespace App\Tests\Unit\Scolarite\Domain\Model\TeacherReplacement;

use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Event\RemplacementDesigne;
use App\Scolarite\Domain\Event\RemplacementTermine;
use App\Scolarite\Domain\Exception\DatesRemplacementInvalidesException;
use App\Scolarite\Domain\Exception\RemplacementDejaTermineException;
use App\Scolarite\Domain\Exception\RemplacementSameTeacherException;
use App\Scolarite\Domain\Model\TeacherReplacement\ClassSubjectPair;
use App\Scolarite\Domain\Model\TeacherReplacement\ReplacementStatus;
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacement;
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacementId;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class TeacherReplacementTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string REPLACED_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
    private const string REPLACEMENT_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440011';
    private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
    private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
    private const string CREATED_BY_ID = '550e8400-e29b-41d4-a716-446655440099';

    #[Test]
    public function designerCreatesActiveReplacement(): void
    {
        $replacement = $this->createReplacement();

        self::assertSame(ReplacementStatus::ACTIVE, $replacement->status);
        self::assertNull($replacement->endedAt);
    }

    #[Test]
    public function designerRecordsRemplacementDesigneEvent(): void
    {
        $replacement = $this->createReplacement();

        $events = $replacement->pullDomainEvents();

        self::assertCount(1, $events);
        self::assertInstanceOf(RemplacementDesigne::class, $events[0]);
        self::assertSame($replacement->id, $events[0]->replacementId);
        self::assertTrue($replacement->replacedTeacherId->equals($events[0]->replacedTeacherId));
        self::assertTrue($replacement->replacementTeacherId->equals($events[0]->replacementTeacherId));
    }

    #[Test]
    public function designerSetsAllProperties(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $replacedTeacherId = UserId::fromString(self::REPLACED_TEACHER_ID);
        $replacementTeacherId = UserId::fromString(self::REPLACEMENT_TEACHER_ID);
        $startDate = new DateTimeImmutable('2026-03-01');
        $endDate = new DateTimeImmutable('2026-03-31');
        $classes = $this->createClasses();
        $createdBy = UserId::fromString(self::CREATED_BY_ID);
        $now = new DateTimeImmutable('2026-02-15 10:00:00');

        $replacement = TeacherReplacement::designer(
            tenantId: $tenantId,
            replacedTeacherId: $replacedTeacherId,
            replacementTeacherId: $replacementTeacherId,
            startDate: $startDate,
            endDate: $endDate,
            classes: $classes,
            reason: 'Congé maladie',
            createdBy: $createdBy,
            now: $now,
        );

        self::assertTrue($replacement->tenantId->equals($tenantId));
        self::assertTrue($replacement->replacedTeacherId->equals($replacedTeacherId));
        self::assertTrue($replacement->replacementTeacherId->equals($replacementTeacherId));
        self::assertEquals($startDate, $replacement->startDate);
        self::assertEquals($endDate, $replacement->endDate);
        self::assertCount(1, $replacement->classes);
        self::assertSame('Congé maladie', $replacement->reason);
        self::assertTrue($replacement->createdBy->equals($createdBy));
        self::assertEquals($now, $replacement->createdAt);
        self::assertEquals($now, $replacement->updatedAt);
        self::assertNull($replacement->endedAt);
    }

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

        $sameTeacherId = UserId::fromString(self::REPLACED_TEACHER_ID);

        TeacherReplacement::designer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            replacedTeacherId: $sameTeacherId,
            replacementTeacherId: $sameTeacherId,
            startDate: new DateTimeImmutable('2026-03-01'),
            endDate: new DateTimeImmutable('2026-03-31'),
            classes: $this->createClasses(),
            reason: null,
            createdBy: UserId::fromString(self::CREATED_BY_ID),
            now: new DateTimeImmutable('2026-02-15 10:00:00'),
        );
    }

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

        TeacherReplacement::designer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            replacedTeacherId: UserId::fromString(self::REPLACED_TEACHER_ID),
            replacementTeacherId: UserId::fromString(self::REPLACEMENT_TEACHER_ID),
            startDate: new DateTimeImmutable('2026-03-31'),
            endDate: new DateTimeImmutable('2026-03-01'),
            classes: $this->createClasses(),
            reason: null,
            createdBy: UserId::fromString(self::CREATED_BY_ID),
            now: new DateTimeImmutable('2026-02-15 10:00:00'),
        );
    }

    #[Test]
    public function isActiveReturnsTrueWhenWithinDateRange(): void
    {
        $replacement = $this->createReplacement();

        self::assertTrue($replacement->isActive(new DateTimeImmutable('2026-03-15')));
    }

    #[Test]
    public function isActiveReturnsTrueOnStartDate(): void
    {
        $replacement = $this->createReplacement();

        self::assertTrue($replacement->isActive(new DateTimeImmutable('2026-03-01')));
    }

    #[Test]
    public function isActiveReturnsTrueOnEndDate(): void
    {
        $replacement = $this->createReplacement();

        self::assertTrue($replacement->isActive(new DateTimeImmutable('2026-03-31')));
    }

    #[Test]
    public function isActiveReturnsFalseBeforeStartDate(): void
    {
        $replacement = $this->createReplacement();

        self::assertFalse($replacement->isActive(new DateTimeImmutable('2026-02-28')));
    }

    #[Test]
    public function isActiveReturnsFalseAfterEndDate(): void
    {
        $replacement = $this->createReplacement();

        self::assertFalse($replacement->isActive(new DateTimeImmutable('2026-04-01')));
    }

    #[Test]
    public function isActiveReturnsFalseWhenEnded(): void
    {
        $replacement = $this->createReplacement();
        $replacement->terminer(new DateTimeImmutable('2026-03-15'));

        self::assertFalse($replacement->isActive(new DateTimeImmutable('2026-03-15')));
    }

    #[Test]
    public function isExpiredReturnsTrueAfterEndDate(): void
    {
        $replacement = $this->createReplacement();

        self::assertTrue($replacement->isExpired(new DateTimeImmutable('2026-04-01')));
    }

    #[Test]
    public function isExpiredReturnsFalseWhenStillActive(): void
    {
        $replacement = $this->createReplacement();

        self::assertFalse($replacement->isExpired(new DateTimeImmutable('2026-03-15')));
    }

    #[Test]
    public function isExpiredReturnsFalseWhenAlreadyEnded(): void
    {
        $replacement = $this->createReplacement();
        $replacement->terminer(new DateTimeImmutable('2026-03-15'));

        self::assertFalse($replacement->isExpired(new DateTimeImmutable('2026-04-01')));
    }

    #[Test]
    public function terminerChangesStatusAndRecordsEvent(): void
    {
        $replacement = $this->createReplacement();
        $replacement->pullDomainEvents();
        $at = new DateTimeImmutable('2026-03-15 14:00:00');

        $replacement->terminer($at);

        self::assertSame(ReplacementStatus::ENDED, $replacement->status);
        self::assertEquals($at, $replacement->endedAt);
        self::assertEquals($at, $replacement->updatedAt);

        $events = $replacement->pullDomainEvents();
        self::assertCount(1, $events);
        self::assertInstanceOf(RemplacementTermine::class, $events[0]);
        self::assertSame($replacement->id, $events[0]->replacementId);
    }

    #[Test]
    public function terminerThrowsWhenAlreadyEnded(): void
    {
        $replacement = $this->createReplacement();
        $replacement->terminer(new DateTimeImmutable('2026-03-15'));

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

        $replacement->terminer(new DateTimeImmutable('2026-03-16'));
    }

    #[Test]
    public function couvreClasseMatiereReturnsTrueForMatchingPair(): void
    {
        $replacement = $this->createReplacement();
        $pair = new ClassSubjectPair(
            ClassId::fromString(self::CLASS_ID),
            SubjectId::fromString(self::SUBJECT_ID),
        );

        self::assertTrue($replacement->couvreClasseMatiere($pair));
    }

    #[Test]
    public function couvreClasseMatiereReturnsFalseForNonMatchingPair(): void
    {
        $replacement = $this->createReplacement();
        $pair = new ClassSubjectPair(
            ClassId::fromString('550e8400-e29b-41d4-a716-446655440099'),
            SubjectId::fromString(self::SUBJECT_ID),
        );

        self::assertFalse($replacement->couvreClasseMatiere($pair));
    }

    #[Test]
    public function reconstituteRestoresAllPropertiesWithoutEvents(): void
    {
        $id = TeacherReplacementId::generate();
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $replacedTeacherId = UserId::fromString(self::REPLACED_TEACHER_ID);
        $replacementTeacherId = UserId::fromString(self::REPLACEMENT_TEACHER_ID);
        $startDate = new DateTimeImmutable('2026-03-01');
        $endDate = new DateTimeImmutable('2026-03-31');
        $classes = $this->createClasses();
        $createdBy = UserId::fromString(self::CREATED_BY_ID);
        $createdAt = new DateTimeImmutable('2026-02-15 10:00:00');
        $endedAt = new DateTimeImmutable('2026-03-20 10:00:00');
        $updatedAt = new DateTimeImmutable('2026-03-20 10:00:00');

        $replacement = TeacherReplacement::reconstitute(
            id: $id,
            tenantId: $tenantId,
            replacedTeacherId: $replacedTeacherId,
            replacementTeacherId: $replacementTeacherId,
            startDate: $startDate,
            endDate: $endDate,
            status: ReplacementStatus::ENDED,
            classes: $classes,
            reason: 'Congé maladie',
            createdBy: $createdBy,
            createdAt: $createdAt,
            endedAt: $endedAt,
            updatedAt: $updatedAt,
        );

        self::assertTrue($replacement->id->equals($id));
        self::assertTrue($replacement->tenantId->equals($tenantId));
        self::assertTrue($replacement->replacedTeacherId->equals($replacedTeacherId));
        self::assertTrue($replacement->replacementTeacherId->equals($replacementTeacherId));
        self::assertEquals($startDate, $replacement->startDate);
        self::assertEquals($endDate, $replacement->endDate);
        self::assertSame(ReplacementStatus::ENDED, $replacement->status);
        self::assertCount(1, $replacement->classes);
        self::assertSame('Congé maladie', $replacement->reason);
        self::assertTrue($replacement->createdBy->equals($createdBy));
        self::assertEquals($createdAt, $replacement->createdAt);
        self::assertEquals($endedAt, $replacement->endedAt);
        self::assertEquals($updatedAt, $replacement->updatedAt);
        self::assertEmpty($replacement->pullDomainEvents());
    }

    private function createReplacement(): TeacherReplacement
    {
        return TeacherReplacement::designer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            replacedTeacherId: UserId::fromString(self::REPLACED_TEACHER_ID),
            replacementTeacherId: UserId::fromString(self::REPLACEMENT_TEACHER_ID),
            startDate: new DateTimeImmutable('2026-03-01'),
            endDate: new DateTimeImmutable('2026-03-31'),
            classes: $this->createClasses(),
            reason: null,
            createdBy: UserId::fromString(self::CREATED_BY_ID),
            now: new DateTimeImmutable('2026-02-15 10:00:00'),
        );
    }

    /** @return array<ClassSubjectPair> */
    private function createClasses(): array
    {
        return [
            new ClassSubjectPair(
                ClassId::fromString(self::CLASS_ID),
                SubjectId::fromString(self::SUBJECT_ID),
            ),
        ];
    }
}
