<?php

declare(strict_types=1);

namespace App\Tests\Unit\Scolarite\Application\Service;

use App\Administration\Domain\Model\SchoolCalendar\CalendarEntry;
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryId;
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
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\Service\ScheduleResolver;
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
use App\Scolarite\Domain\Model\Schedule\ScheduleException;
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\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class ScheduleResolverTest 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 ScheduleResolver $resolver;

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

    #[Test]
    public function resolveForWeekReturnsRecurringSlotsForSchoolDays(): void
    {
        $slot = $this->createAndSaveSlot(DayOfWeek::MONDAY, '08:00', '09:00');

        $calendar = $this->createCalendar();
        // 2026-09-07 is a Monday
        $weekStart = new DateTimeImmutable('2026-09-07');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

        self::assertCount(1, $resolved);
        self::assertTrue($resolved[0]->slotId->equals($slot->id));
        self::assertSame('2026-09-07', $resolved[0]->date->format('Y-m-d'));
        self::assertFalse($resolved[0]->isModified);
    }

    #[Test]
    public function resolveForWeekReturnsMultipleSlotsOnDifferentDays(): void
    {
        $this->createAndSaveSlot(DayOfWeek::MONDAY, '08:00', '09:00');
        $this->createAndSaveSlot(DayOfWeek::WEDNESDAY, '10:00', '11:00');
        $this->createAndSaveSlot(DayOfWeek::FRIDAY, '14:00', '15:00');

        $calendar = $this->createCalendar();
        $weekStart = new DateTimeImmutable('2026-09-07');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

        self::assertCount(3, $resolved);
    }

    #[Test]
    public function resolveForWeekSkipsCancelledOccurrences(): void
    {
        $slot = $this->createAndSaveSlot(DayOfWeek::MONDAY, '08:00', '09:00');

        $exception = ScheduleException::annuler(
            tenantId: TenantId::fromString(self::TENANT_ID),
            slotId: $slot->id,
            exceptionDate: new DateTimeImmutable('2026-09-07'),
            reason: 'Grève',
            createdBy: UserId::fromString(self::CREATOR_ID),
            now: new DateTimeImmutable('2026-09-01 10:00:00'),
        );
        $this->exceptionRepository->save($exception);

        $calendar = $this->createCalendar();
        $weekStart = new DateTimeImmutable('2026-09-07');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

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

    #[Test]
    public function resolveForWeekAppliesModifiedExceptionOverrides(): void
    {
        $slot = $this->createAndSaveSlot(DayOfWeek::MONDAY, '08:00', '09:00');

        $newTimeSlot = new TimeSlot('10:00', '11:00');
        $newTeacherId = UserId::fromString('550e8400-e29b-41d4-a716-446655440011');
        $exception = ScheduleException::modifier(
            tenantId: TenantId::fromString(self::TENANT_ID),
            slotId: $slot->id,
            exceptionDate: new DateTimeImmutable('2026-09-07'),
            newTimeSlot: $newTimeSlot,
            newRoom: 'Salle 301',
            newTeacherId: $newTeacherId,
            reason: 'Changement de salle',
            createdBy: UserId::fromString(self::CREATOR_ID),
            now: new DateTimeImmutable('2026-09-01 10:00:00'),
        );
        $this->exceptionRepository->save($exception);

        $calendar = $this->createCalendar();
        $weekStart = new DateTimeImmutable('2026-09-07');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

        self::assertCount(1, $resolved);
        self::assertTrue($resolved[0]->isModified);
        self::assertSame('10:00', $resolved[0]->timeSlot->startTime);
        self::assertSame('11:00', $resolved[0]->timeSlot->endTime);
        self::assertSame('Salle 301', $resolved[0]->room);
        self::assertTrue($resolved[0]->teacherId->equals($newTeacherId));
    }

    #[Test]
    public function resolveForWeekSkipsNonSchoolDays(): void
    {
        // Saturday slot — should not appear (weekend)
        $this->createAndSaveSlot(DayOfWeek::SATURDAY, '08:00', '09:00');

        $calendar = $this->createCalendar();
        $weekStart = new DateTimeImmutable('2026-09-07');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

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

    #[Test]
    public function resolveForWeekSkipsVacationDays(): void
    {
        // Monday slot, but this Monday is during vacation
        $this->createAndSaveSlot(DayOfWeek::MONDAY, '08:00', '09:00');

        $calendar = $this->createCalendarWithVacation(
            new DateTimeImmutable('2026-10-19'),
            new DateTimeImmutable('2026-11-01'),
        );

        // Week starting Monday Oct 19 (during vacation)
        $weekStart = new DateTimeImmutable('2026-10-19');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

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

    #[Test]
    public function resolveForWeekSkipsHolidays(): void
    {
        // Monday slot
        $this->createAndSaveSlot(DayOfWeek::MONDAY, '08:00', '09:00');
        // Wednesday slot
        $this->createAndSaveSlot(DayOfWeek::WEDNESDAY, '10:00', '11:00');

        // Nov 11 2026 is a Wednesday (holiday: Armistice)
        $calendar = $this->createCalendarWithHoliday(new DateTimeImmutable('2026-11-11'));

        // Week of Nov 9 2026 (Monday)
        $weekStart = new DateTimeImmutable('2026-11-09');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

        // Monday should appear, Wednesday (holiday) should not
        self::assertCount(1, $resolved);
        self::assertSame('2026-11-09', $resolved[0]->date->format('Y-m-d'));
    }

    #[Test]
    public function resolveForWeekRespectsRecurrenceBounds(): void
    {
        // Slot with recurrence starting Sep 15
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $slot = ScheduleSlot::creer(
            tenantId: $tenantId,
            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-15'),
            recurrenceEnd: new DateTimeImmutable('2027-07-04'),
        );
        $this->slotRepository->save($slot);

        $calendar = $this->createCalendar();

        // Week of Sep 7 — before recurrence start
        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            new DateTimeImmutable('2026-09-07'),
            $tenantId,
            $calendar,
        );
        self::assertCount(0, $resolved);

        // Week of Sep 15 — within recurrence bounds
        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            new DateTimeImmutable('2026-09-15'),
            $tenantId,
            $calendar,
        );
        self::assertCount(1, $resolved);
    }

    #[Test]
    public function resolveForWeekIgnoresNonRecurringSlots(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $slot = ScheduleSlot::creer(
            tenantId: $tenantId,
            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: false,
            now: new DateTimeImmutable('2026-09-01 10:00:00'),
        );
        $this->slotRepository->save($slot);

        $calendar = $this->createCalendar();
        $weekStart = new DateTimeImmutable('2026-09-07');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            $tenantId,
            $calendar,
        );

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

    #[Test]
    public function resolveForWeekReturnsEmptyForClassWithNoSlots(): void
    {
        $calendar = $this->createCalendar();
        $weekStart = new DateTimeImmutable('2026-09-07');

        $resolved = $this->resolver->resolveForWeek(
            ClassId::fromString(self::CLASS_ID),
            $weekStart,
            TenantId::fromString(self::TENANT_ID),
            $calendar,
        );

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

    private function createAndSaveSlot(
        DayOfWeek $dayOfWeek,
        string $startTime,
        string $endTime,
        ?string $room = null,
    ): 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,
            timeSlot: new TimeSlot($startTime, $endTime),
            room: $room,
            isRecurring: true,
            now: new DateTimeImmutable('2026-09-01 10:00:00'),
        );

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

        return $slot;
    }

    private function createCalendar(): SchoolCalendar
    {
        return SchoolCalendar::reconstitute(
            tenantId: TenantId::fromString(self::TENANT_ID),
            academicYearId: AcademicYearId::fromString('550e8400-e29b-41d4-a716-446655440090'),
            zone: null,
            entries: [],
        );
    }

    private function createCalendarWithVacation(
        DateTimeImmutable $start,
        DateTimeImmutable $end,
    ): SchoolCalendar {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(new CalendarEntry(
            id: CalendarEntryId::generate(),
            type: CalendarEntryType::VACATION,
            startDate: $start,
            endDate: $end,
            label: 'Vacances de la Toussaint',
        ));

        return $calendar;
    }

    private function createCalendarWithHoliday(DateTimeImmutable $date): SchoolCalendar
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(new CalendarEntry(
            id: CalendarEntryId::generate(),
            type: CalendarEntryType::HOLIDAY,
            startDate: $date,
            endDate: $date,
            label: 'Armistice',
        ));

        return $calendar;
    }
}
