<?php

declare(strict_types=1);

namespace App\Tests\Unit\Scolarite\Application\Command\CreateScheduleException;

use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Application\Command\CreateScheduleException\CreateScheduleExceptionCommand;
use App\Scolarite\Application\Command\CreateScheduleException\CreateScheduleExceptionHandler;
use App\Scolarite\Domain\Exception\DateExceptionInvalideException;
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
use App\Scolarite\Domain\Model\Schedule\ScheduleExceptionType;
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleExceptionRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleSlotRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class CreateScheduleExceptionHandlerTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
    private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
    private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
    private const string CREATOR_ID = '550e8400-e29b-41d4-a716-446655440099';

    private InMemoryScheduleSlotRepository $slotRepository;
    private InMemoryScheduleExceptionRepository $exceptionRepository;
    private CreateScheduleExceptionHandler $handler;

    protected function setUp(): void
    {
        $this->slotRepository = new InMemoryScheduleSlotRepository();
        $this->exceptionRepository = new InMemoryScheduleExceptionRepository();

        $clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-10-01 10:00:00');
            }
        };

        $this->handler = new CreateScheduleExceptionHandler(
            $this->slotRepository,
            $this->exceptionRepository,
            $clock,
        );
    }

    #[Test]
    public function cancelOccurrenceCreatesACancelledException(): void
    {
        $slot = $this->createAndSaveSlot();

        $command = new CreateScheduleExceptionCommand(
            tenantId: self::TENANT_ID,
            slotId: (string) $slot->id,
            exceptionDate: '2026-10-05',
            type: 'cancelled',
            reason: 'Grève',
            createdBy: self::CREATOR_ID,
        );

        $exception = ($this->handler)($command);

        self::assertSame(ScheduleExceptionType::CANCELLED, $exception->type);
        self::assertSame('2026-10-05', $exception->exceptionDate->format('Y-m-d'));
        self::assertSame('Grève', $exception->reason);
        self::assertNull($exception->newTimeSlot);
    }

    #[Test]
    public function modifyOccurrenceCreatesAModifiedException(): void
    {
        $slot = $this->createAndSaveSlot();
        $newTeacherId = '550e8400-e29b-41d4-a716-446655440011';

        $command = new CreateScheduleExceptionCommand(
            tenantId: self::TENANT_ID,
            slotId: (string) $slot->id,
            exceptionDate: '2026-10-05',
            type: 'modified',
            newStartTime: '10:00',
            newEndTime: '11:00',
            newRoom: 'Salle 301',
            newTeacherId: $newTeacherId,
            reason: 'Changement de salle',
            createdBy: self::CREATOR_ID,
        );

        $exception = ($this->handler)($command);

        self::assertSame(ScheduleExceptionType::MODIFIED, $exception->type);
        self::assertNotNull($exception->newTimeSlot);
        self::assertSame('10:00', $exception->newTimeSlot->startTime);
        self::assertSame('11:00', $exception->newTimeSlot->endTime);
        self::assertSame('Salle 301', $exception->newRoom);
        self::assertTrue($exception->newTeacherId->equals(UserId::fromString($newTeacherId)));
    }

    #[Test]
    public function exceptionIsSavedToRepository(): void
    {
        $slot = $this->createAndSaveSlot();

        $command = new CreateScheduleExceptionCommand(
            tenantId: self::TENANT_ID,
            slotId: (string) $slot->id,
            exceptionDate: '2026-10-05',
            type: 'cancelled',
            createdBy: self::CREATOR_ID,
        );

        $exception = ($this->handler)($command);
        $tenantId = TenantId::fromString(self::TENANT_ID);

        $found = $this->exceptionRepository->findById($exception->id, $tenantId);
        self::assertNotNull($found);
    }

    #[Test]
    public function throwsWhenSlotNotFound(): void
    {
        $this->expectException(\App\Scolarite\Domain\Exception\ScheduleSlotNotFoundException::class);

        $command = new CreateScheduleExceptionCommand(
            tenantId: self::TENANT_ID,
            slotId: '550e8400-e29b-41d4-a716-446655440099',
            exceptionDate: '2026-10-05',
            type: 'cancelled',
            createdBy: self::CREATOR_ID,
        );

        ($this->handler)($command);
    }

    #[Test]
    public function throwsWhenDateIsWrongDayOfWeek(): void
    {
        $slot = $this->createAndSaveSlot();

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

        // Slot is MONDAY, but 2026-10-06 is a Tuesday
        $command = new CreateScheduleExceptionCommand(
            tenantId: self::TENANT_ID,
            slotId: (string) $slot->id,
            exceptionDate: '2026-10-06',
            type: 'cancelled',
            createdBy: self::CREATOR_ID,
        );

        ($this->handler)($command);
    }

    #[Test]
    public function throwsWhenDateIsOutsideRecurrenceBounds(): void
    {
        $slot = ScheduleSlot::creer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            classId: ClassId::fromString(self::CLASS_ID),
            subjectId: SubjectId::fromString(self::SUBJECT_ID),
            teacherId: UserId::fromString(self::TEACHER_ID),
            dayOfWeek: DayOfWeek::MONDAY,
            timeSlot: new TimeSlot('08:00', '09:00'),
            room: null,
            isRecurring: true,
            now: new DateTimeImmutable('2026-09-01 10:00:00'),
            recurrenceStart: new DateTimeImmutable('2026-09-07'),
            recurrenceEnd: new DateTimeImmutable('2026-09-28'),
        );
        $this->slotRepository->save($slot);

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

        // 2026-10-05 is a Monday but after recurrenceEnd (2026-09-28)
        $command = new CreateScheduleExceptionCommand(
            tenantId: self::TENANT_ID,
            slotId: (string) $slot->id,
            exceptionDate: '2026-10-05',
            type: 'cancelled',
            createdBy: self::CREATOR_ID,
        );

        ($this->handler)($command);
    }

    private function createAndSaveSlot(): ScheduleSlot
    {
        $slot = ScheduleSlot::creer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            classId: ClassId::fromString(self::CLASS_ID),
            subjectId: SubjectId::fromString(self::SUBJECT_ID),
            teacherId: UserId::fromString(self::TEACHER_ID),
            dayOfWeek: DayOfWeek::MONDAY,
            timeSlot: new TimeSlot('08:00', '09:00'),
            room: null,
            isRecurring: true,
            now: new DateTimeImmutable('2026-09-01 10:00:00'),
        );

        $this->slotRepository->save($slot);

        return $slot;
    }
}
