<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Exception\CalendrierNonTrouveException;
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\Administration\Infrastructure\Persistence\Doctrine\DoctrineSchoolCalendarRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class DoctrineSchoolCalendarRepositoryTest extends TestCase
{
    private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
    private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';

    #[Test]
    public function saveDeletesExistingEntriesThenInsertsNew(): void
    {
        $connection = $this->createMock(Connection::class);

        $calendar = SchoolCalendar::initialiser(
            tenantId: TenantId::fromString(self::TENANT_ID),
            academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );
        $calendar->ajouterEntree(new CalendarEntry(
            id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440020'),
            type: CalendarEntryType::HOLIDAY,
            startDate: new DateTimeImmutable('2025-11-01'),
            endDate: new DateTimeImmutable('2025-11-01'),
            label: 'Toussaint',
        ));

        $connection->expects(self::once())
            ->method('transactional')
            ->willReturnCallback(static function (callable $callback) { $callback(); });

        // Expect: 1 DELETE + 1 INSERT
        $connection->expects(self::exactly(2))
            ->method('executeStatement')
            ->with(
                self::logicalOr(
                    self::stringContains('DELETE FROM school_calendar_entries'),
                    self::stringContains('INSERT INTO school_calendar_entries'),
                ),
                self::isType('array'),
            );

        $repository = new DoctrineSchoolCalendarRepository($connection);
        $repository->save($calendar);
    }

    #[Test]
    public function saveWithMultipleEntriesExecutesMultipleInserts(): void
    {
        $connection = $this->createMock(Connection::class);

        $calendar = SchoolCalendar::initialiser(
            tenantId: TenantId::fromString(self::TENANT_ID),
            academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );
        $calendar->ajouterEntree(new CalendarEntry(
            id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440020'),
            type: CalendarEntryType::HOLIDAY,
            startDate: new DateTimeImmutable('2025-11-01'),
            endDate: new DateTimeImmutable('2025-11-01'),
            label: 'Toussaint',
        ));
        $calendar->ajouterEntree(new CalendarEntry(
            id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440021'),
            type: CalendarEntryType::VACATION,
            startDate: new DateTimeImmutable('2025-12-21'),
            endDate: new DateTimeImmutable('2026-01-05'),
            label: 'Noël',
        ));

        $connection->expects(self::once())
            ->method('transactional')
            ->willReturnCallback(static function (callable $callback) { $callback(); });

        // Expect: 1 DELETE + 2 INSERTs = 3 calls
        $connection->expects(self::exactly(3))
            ->method('executeStatement');

        $repository = new DoctrineSchoolCalendarRepository($connection);
        $repository->save($calendar);
    }

    #[Test]
    public function findByTenantAndYearReturnsNullWhenNoEntries(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAllAssociative')->willReturn([]);

        $repository = new DoctrineSchoolCalendarRepository($connection);

        $calendar = $repository->findByTenantAndYear(
            TenantId::fromString(self::TENANT_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );

        self::assertNull($calendar);
    }

    #[Test]
    public function findByTenantAndYearReturnsCalendarWithEntries(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAllAssociative')
            ->willReturn([
                $this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'holiday', '2025-11-01', '2025-11-01', 'Toussaint', null, 'A'),
                $this->makeRow('550e8400-e29b-41d4-a716-446655440021', 'vacation', '2025-12-21', '2026-01-05', 'Noël', 'Vacances de Noël', 'A'),
            ]);

        $repository = new DoctrineSchoolCalendarRepository($connection);

        $calendar = $repository->findByTenantAndYear(
            TenantId::fromString(self::TENANT_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );

        self::assertNotNull($calendar);
        self::assertCount(2, $calendar->entries());
        self::assertSame(SchoolZone::A, $calendar->zone);
        self::assertSame('Toussaint', $calendar->entries()[0]->label);
        self::assertSame('Noël', $calendar->entries()[1]->label);
    }

    #[Test]
    public function findByTenantAndYearHandlesNullZone(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAllAssociative')
            ->willReturn([
                $this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'pedagogical', '2025-03-14', '2025-03-14', 'Formation', null, null),
            ]);

        $repository = new DoctrineSchoolCalendarRepository($connection);

        $calendar = $repository->findByTenantAndYear(
            TenantId::fromString(self::TENANT_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );

        self::assertNotNull($calendar);
        self::assertNull($calendar->zone);
    }

    #[Test]
    public function getByTenantAndYearThrowsWhenNotFound(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAllAssociative')->willReturn([]);

        $repository = new DoctrineSchoolCalendarRepository($connection);

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

        $repository->getByTenantAndYear(
            TenantId::fromString(self::TENANT_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );
    }

    #[Test]
    public function getByTenantAndYearReturnsCalendarWhenFound(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAllAssociative')
            ->willReturn([
                $this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'holiday', '2025-05-01', '2025-05-01', 'Fête du travail', null, 'B'),
            ]);

        $repository = new DoctrineSchoolCalendarRepository($connection);

        $calendar = $repository->getByTenantAndYear(
            TenantId::fromString(self::TENANT_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );

        self::assertCount(1, $calendar->entries());
        self::assertSame(SchoolZone::B, $calendar->zone);
    }

    /**
     * @return array<string, mixed>
     */
    private function makeRow(
        string $id,
        string $entryType,
        string $startDate,
        string $endDate,
        string $label,
        ?string $description,
        ?string $zone,
    ): array {
        return [
            'id' => $id,
            'tenant_id' => self::TENANT_ID,
            'academic_year_id' => self::ACADEMIC_YEAR_ID,
            'entry_type' => $entryType,
            'start_date' => $startDate,
            'end_date' => $endDate,
            'label' => $label,
            'description' => $description,
            'zone' => $zone,
            'created_at' => '2026-02-17T10:00:00+00:00',
        ];
    }
}
