<?php

declare(strict_types=1);

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

use App\Administration\Domain\Event\CalendrierConfigure;
use App\Administration\Domain\Event\JourneePedagogiqueAjoutee;
use App\Administration\Domain\Exception\CalendrierEntreeNonTrouveeException;
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\SchoolCalendar\SchoolZone;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;

use function sprintf;

final class SchoolCalendarTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';

    #[Test]
    public function initialiserCreeCalendrierVide(): void
    {
        $calendar = $this->createCalendar();

        self::assertTrue($calendar->tenantId->equals(TenantId::fromString(self::TENANT_ID)));
        self::assertTrue($calendar->academicYearId->equals(AcademicYearId::fromString(self::ACADEMIC_YEAR_ID)));
        self::assertNull($calendar->zone);
        self::assertEmpty($calendar->entries());
    }

    #[Test]
    public function configurerZoneDefInitLaZone(): void
    {
        $calendar = $this->createCalendar();

        $calendar->configurerZone(SchoolZone::A);

        self::assertSame(SchoolZone::A, $calendar->zone);
    }

    #[Test]
    public function configurerZonePeutEtreChangee(): void
    {
        $calendar = $this->createCalendar();

        $calendar->configurerZone(SchoolZone::A);
        $calendar->configurerZone(SchoolZone::C);

        self::assertSame(SchoolZone::C, $calendar->zone);
    }

    #[Test]
    public function ajouterEntreeAjouteAuCalendrier(): void
    {
        $calendar = $this->createCalendar();
        $entry = $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint');

        $calendar->ajouterEntree($entry);

        self::assertCount(1, $calendar->entries());
        self::assertSame($entry, $calendar->entries()[0]);
    }

    #[Test]
    public function ajouterPlusieursEntrees(): void
    {
        $calendar = $this->createCalendar();

        $calendar->ajouterEntree($this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint'));
        $calendar->ajouterEntree($this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-11', '2024-11-11', 'Armistice'));
        $calendar->ajouterEntree($this->createEntry(CalendarEntryType::VACATION, '2024-12-21', '2025-01-05', 'Vacances de Noël'));

        self::assertCount(3, $calendar->entries());
    }

    #[Test]
    public function supprimerEntreeRetireEntreeExistante(): void
    {
        $calendar = $this->createCalendar();
        $entry = $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint');

        $calendar->ajouterEntree($entry);
        $calendar->supprimerEntree($entry->id);

        self::assertEmpty($calendar->entries());
    }

    #[Test]
    public function supprimerEntreeInexistanteLeveException(): void
    {
        $calendar = $this->createCalendar();

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

        $calendar->supprimerEntree(CalendarEntryId::generate());
    }

    #[Test]
    public function viderEntreesSupprimeTout(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree($this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint'));
        $calendar->ajouterEntree($this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-11', '2024-11-11', 'Armistice'));

        $calendar->viderEntrees();

        self::assertEmpty($calendar->entries());
    }

    #[Test]
    public function estJourOuvreRetourneTruePourJourSemaine(): void
    {
        $calendar = $this->createCalendar();

        // Lundi 4 novembre 2024
        self::assertTrue($calendar->estJourOuvre(new DateTimeImmutable('2024-11-04')));
    }

    #[Test]
    public function estJourOuvreRetourneFalsePourSamedi(): void
    {
        $calendar = $this->createCalendar();

        // Samedi 2 novembre 2024
        self::assertFalse($calendar->estJourOuvre(new DateTimeImmutable('2024-11-02')));
    }

    #[Test]
    public function estJourOuvreRetourneFalsePourDimanche(): void
    {
        $calendar = $this->createCalendar();

        // Dimanche 3 novembre 2024
        self::assertFalse($calendar->estJourOuvre(new DateTimeImmutable('2024-11-03')));
    }

    #[Test]
    public function estJourOuvreRetourneFalsePourJourFerie(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint'),
        );

        // Vendredi 1er novembre 2024 (Toussaint)
        self::assertFalse($calendar->estJourOuvre(new DateTimeImmutable('2024-11-01')));
    }

    #[Test]
    public function estJourOuvreRetourneFalsePendantVacances(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::VACATION, '2024-10-19', '2024-11-03', 'Vacances Toussaint'),
        );

        // Mercredi 23 octobre 2024 (en plein dans les vacances)
        self::assertFalse($calendar->estJourOuvre(new DateTimeImmutable('2024-10-23')));
    }

    #[Test]
    public function estJourOuvreRetourneFalsePourJourneePedagogique(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::PEDAGOGICAL_DAY, '2025-03-14', '2025-03-14', 'Formation'),
        );

        // Vendredi 14 mars 2025
        self::assertFalse($calendar->estJourOuvre(new DateTimeImmutable('2025-03-14')));
    }

    #[Test]
    public function estJourOuvreRetourneTrueApresVacances(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::VACATION, '2024-10-19', '2024-11-03', 'Vacances Toussaint'),
        );

        // Lundi 4 novembre 2024 (jour de reprise)
        self::assertTrue($calendar->estJourOuvre(new DateTimeImmutable('2024-11-04')));
    }

    #[Test]
    public function trouverEntreePourDateRetourneEntreeCorrespondante(): void
    {
        $calendar = $this->createCalendar();
        $holiday = $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint');
        $calendar->ajouterEntree($holiday);

        $found = $calendar->trouverEntreePourDate(new DateTimeImmutable('2024-11-01'));

        self::assertNotNull($found);
        self::assertSame('Toussaint', $found->label);
    }

    #[Test]
    public function trouverEntreePourDateRetourneNullSiAucune(): void
    {
        $calendar = $this->createCalendar();

        $found = $calendar->trouverEntreePourDate(new DateTimeImmutable('2024-11-01'));

        self::assertNull($found);
    }

    #[Test]
    public function estEnVacancesRetourneTruePendantVacances(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::VACATION, '2024-10-19', '2024-11-03', 'Vacances Toussaint'),
        );

        self::assertTrue($calendar->estEnVacances(new DateTimeImmutable('2024-10-25')));
    }

    #[Test]
    public function estEnVacancesRetourneFalseHorsVacances(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::VACATION, '2024-10-19', '2024-11-03', 'Vacances Toussaint'),
        );

        self::assertFalse($calendar->estEnVacances(new DateTimeImmutable('2024-11-04')));
    }

    #[Test]
    public function estEnVacancesRetourneFalsePourJourFerie(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint'),
        );

        self::assertFalse($calendar->estEnVacances(new DateTimeImmutable('2024-11-01')));
    }

    #[Test]
    public function estJourRetourVacancesRetourneTruePourJourApresFinVacances(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::VACATION, '2024-10-19', '2024-11-03', 'Vacances Toussaint'),
        );

        // 4 novembre = lendemain de la fin des vacances (3 novembre)
        self::assertTrue($calendar->estJourRetourVacances(new DateTimeImmutable('2024-11-04')));
    }

    #[Test]
    public function estJourRetourVacancesRetourneFalsePourJourNormal(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree(
            $this->createEntry(CalendarEntryType::VACATION, '2024-10-19', '2024-11-03', 'Vacances Toussaint'),
        );

        self::assertFalse($calendar->estJourRetourVacances(new DateTimeImmutable('2024-11-05')));
    }

    #[Test]
    public function configurerDefinitZoneEtImporteEntreesAvecEvenement(): void
    {
        $calendar = $this->createCalendar();
        $entries = [
            $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint'),
            $this->createEntry(CalendarEntryType::VACATION, '2024-10-19', '2024-11-03', 'Vacances Toussaint'),
        ];
        $at = new DateTimeImmutable('2026-02-17 10:00:00');

        $calendar->configurer(SchoolZone::A, $entries, $at);

        self::assertSame(SchoolZone::A, $calendar->zone);
        self::assertCount(2, $calendar->entries());

        $events = $calendar->pullDomainEvents();
        self::assertCount(1, $events);
        self::assertInstanceOf(CalendrierConfigure::class, $events[0]);
        self::assertSame(SchoolZone::A, $events[0]->zone);
        self::assertSame(2, $events[0]->nombreEntrees);

        $expectedAggregateId = Uuid::uuid5(
            Uuid::NAMESPACE_DNS,
            sprintf('school-calendar:%s:%s', self::TENANT_ID, self::ACADEMIC_YEAR_ID),
        );
        self::assertTrue($events[0]->aggregateId()->equals($expectedAggregateId));
    }

    #[Test]
    public function configurerPreserveJourneesPedagogiques(): void
    {
        $calendar = $this->createCalendar();
        $pedaEntry = $this->createEntry(CalendarEntryType::PEDAGOGICAL_DAY, '2025-03-14', '2025-03-14', 'Formation');
        $calendar->ajouterEntree($pedaEntry);
        $calendar->ajouterEntree($this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint'));

        $entries = [
            $this->createEntry(CalendarEntryType::VACATION, '2024-12-21', '2025-01-05', 'Vacances de Noël'),
        ];

        $calendar->configurer(SchoolZone::A, $entries, new DateTimeImmutable());

        // 1 preserved pedagogical day + 1 new vacation = 2
        self::assertCount(2, $calendar->entries());

        $types = array_map(static fn (CalendarEntry $e) => $e->type, $calendar->entries());
        self::assertContains(CalendarEntryType::PEDAGOGICAL_DAY, $types);
        self::assertContains(CalendarEntryType::VACATION, $types);
        self::assertNotContains(CalendarEntryType::HOLIDAY, $types);
    }

    #[Test]
    public function configurerReemplaceEntreesExistantes(): void
    {
        $calendar = $this->createCalendar();
        $calendar->ajouterEntree($this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Ancienne'));

        $entries = [
            $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-11', '2024-11-11', 'Armistice'),
        ];

        $calendar->configurer(SchoolZone::B, $entries, new DateTimeImmutable());

        self::assertCount(1, $calendar->entries());
        self::assertSame('Armistice', $calendar->entries()[0]->label);
    }

    #[Test]
    public function ajouterJourneePedagogiqueEmetEvenement(): void
    {
        $calendar = $this->createCalendar();
        $entry = $this->createEntry(CalendarEntryType::PEDAGOGICAL_DAY, '2025-03-14', '2025-03-14', 'Formation enseignants');
        $at = new DateTimeImmutable('2026-02-17 10:00:00');

        $calendar->ajouterJourneePedagogique($entry, $at);

        self::assertCount(1, $calendar->entries());

        $events = $calendar->pullDomainEvents();
        self::assertCount(1, $events);
        self::assertInstanceOf(JourneePedagogiqueAjoutee::class, $events[0]);
        self::assertSame('Formation enseignants', $events[0]->label);
        self::assertSame('2025-03-14', $events[0]->date->format('Y-m-d'));

        $expectedAggregateId = Uuid::uuid5(
            Uuid::NAMESPACE_DNS,
            sprintf('school-calendar:%s:%s', self::TENANT_ID, self::ACADEMIC_YEAR_ID),
        );
        self::assertTrue($events[0]->aggregateId()->equals($expectedAggregateId));
    }

    #[Test]
    public function ajouterJourneePedagogiqueRefuseTypeDifferent(): void
    {
        $calendar = $this->createCalendar();
        $entry = $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint');

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

        $calendar->ajouterJourneePedagogique($entry, new DateTimeImmutable());
    }

    #[Test]
    public function reconstituteRestaureLEtat(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $yearId = AcademicYearId::fromString(self::ACADEMIC_YEAR_ID);
        $entry1 = $this->createEntry(CalendarEntryType::HOLIDAY, '2024-11-01', '2024-11-01', 'Toussaint');
        $entry2 = $this->createEntry(CalendarEntryType::VACATION, '2024-12-21', '2025-01-05', 'Noël');

        $calendar = SchoolCalendar::reconstitute(
            tenantId: $tenantId,
            academicYearId: $yearId,
            zone: SchoolZone::B,
            entries: [$entry1, $entry2],
        );

        self::assertTrue($calendar->tenantId->equals($tenantId));
        self::assertTrue($calendar->academicYearId->equals($yearId));
        self::assertSame(SchoolZone::B, $calendar->zone);
        self::assertCount(2, $calendar->entries());
        self::assertEmpty($calendar->pullDomainEvents());
    }

    private function createCalendar(): SchoolCalendar
    {
        return SchoolCalendar::initialiser(
            tenantId: TenantId::fromString(self::TENANT_ID),
            academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );
    }

    private function createEntry(
        CalendarEntryType $type,
        string $startDate,
        string $endDate,
        string $label,
    ): CalendarEntry {
        return new CalendarEntry(
            id: CalendarEntryId::generate(),
            type: $type,
            startDate: new DateTimeImmutable($startDate),
            endDate: new DateTimeImmutable($endDate),
            label: $label,
        );
    }
}
