<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Service;

use App\Administration\Application\Dto\PaginatedResult;
use App\Administration\Application\Port\OfficialCalendarProvider;
use App\Administration\Application\Port\PasswordHasher;
use App\Administration\Application\Service\Cache\PaginatedQueryCache;
use App\Administration\Domain\Model\AcademicYear\PeriodType;
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\SchoolZone;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Repository\GradingConfigurationRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassAssignmentRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryGradingConfigurationRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryPeriodConfigurationRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolCalendarRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySubjectRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryTeacherAssignmentRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Administration\Infrastructure\School\SchoolIdResolver;
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
use App\Administration\Infrastructure\Service\DemoDataGenerator;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleSlotRepository;
use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;

final class DemoDataGeneratorTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440123';
    private const string TENANT_SUBDOMAIN = 'demo';

    private InMemoryUserRepository $userRepository;
    private InMemoryClassRepository $classRepository;
    private InMemorySubjectRepository $subjectRepository;
    private InMemoryClassAssignmentRepository $classAssignmentRepository;
    private InMemoryTeacherAssignmentRepository $teacherAssignmentRepository;
    private InMemoryStudentGuardianRepository $studentGuardianRepository;
    private InMemoryScheduleSlotRepository $scheduleSlotRepository;
    private InMemoryPeriodConfigurationRepository $periodConfigurationRepository;
    private InMemorySchoolCalendarRepository $schoolCalendarRepository;
    private GradingConfigurationRepository $gradingConfigurationRepository;
    private PaginatedQueryCache $paginatedQueryCache;
    private DemoDataGenerator $generator;
    private TenantConfig $tenantConfig;

    protected function setUp(): void
    {
        $this->userRepository = new InMemoryUserRepository();
        $this->classRepository = new InMemoryClassRepository();
        $this->subjectRepository = new InMemorySubjectRepository();
        $this->classAssignmentRepository = new InMemoryClassAssignmentRepository();
        $this->teacherAssignmentRepository = new InMemoryTeacherAssignmentRepository();
        $this->studentGuardianRepository = new InMemoryStudentGuardianRepository();
        $this->scheduleSlotRepository = new InMemoryScheduleSlotRepository();
        $this->periodConfigurationRepository = new InMemoryPeriodConfigurationRepository();
        $this->schoolCalendarRepository = new InMemorySchoolCalendarRepository();
        $this->gradingConfigurationRepository = new InMemoryGradingConfigurationRepository();
        $this->paginatedQueryCache = new PaginatedQueryCache(new TagAwareAdapter(new ArrayAdapter()));

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

        $tenantContext = new TenantContext();
        $currentAcademicYearResolver = new CurrentAcademicYearResolver($tenantContext, $clock);

        $passwordHasher = new class implements PasswordHasher {
            public function hash(string $plainPassword): string
            {
                return 'hashed_' . $plainPassword;
            }

            public function verify(string $hashedPassword, string $plainPassword): bool
            {
                return $hashedPassword === 'hashed_' . $plainPassword;
            }
        };

        $officialCalendarProvider = new class implements OfficialCalendarProvider {
            public function joursFeries(string $academicYear): array
            {
                return [
                    new CalendarEntry(
                        id: CalendarEntryId::generate(),
                        type: CalendarEntryType::HOLIDAY,
                        startDate: new DateTimeImmutable('2025-11-11'),
                        endDate: new DateTimeImmutable('2025-11-11'),
                        label: 'Armistice',
                    ),
                ];
            }

            public function vacancesParZone(SchoolZone $zone, string $academicYear): array
            {
                return [
                    new CalendarEntry(
                        id: CalendarEntryId::generate(),
                        type: CalendarEntryType::VACATION,
                        startDate: new DateTimeImmutable('2025-12-20'),
                        endDate: new DateTimeImmutable('2026-01-04'),
                        label: 'Vacances de Noël',
                    ),
                ];
            }

            public function toutesEntreesOfficielles(SchoolZone $zone, string $academicYear): array
            {
                return [
                    ...$this->joursFeries($academicYear),
                    ...$this->vacancesParZone($zone, $academicYear),
                ];
            }
        };

        $this->generator = new DemoDataGenerator(
            userRepository: $this->userRepository,
            classRepository: $this->classRepository,
            subjectRepository: $this->subjectRepository,
            classAssignmentRepository: $this->classAssignmentRepository,
            teacherAssignmentRepository: $this->teacherAssignmentRepository,
            studentGuardianRepository: $this->studentGuardianRepository,
            scheduleSlotRepository: $this->scheduleSlotRepository,
            periodConfigurationRepository: $this->periodConfigurationRepository,
            schoolCalendarRepository: $this->schoolCalendarRepository,
            gradingConfigurationRepository: $this->gradingConfigurationRepository,
            passwordHasher: $passwordHasher,
            paginatedQueryCache: $this->paginatedQueryCache,
            clock: $clock,
            tenantContext: $tenantContext,
            currentAcademicYearResolver: $currentAcademicYearResolver,
            schoolIdResolver: new SchoolIdResolver(),
            officialCalendarProvider: $officialCalendarProvider,
        );

        $this->tenantConfig = new TenantConfig(
            tenantId: TenantId::fromString(self::TENANT_ID),
            subdomain: self::TENANT_SUBDOMAIN,
            databaseUrl: 'postgresql://localhost/demo',
        );
    }

    #[Test]
    public function itGeneratesACompleteDemoDataset(): void
    {
        $result = $this->generator->generate(
            tenantConfig: $this->tenantConfig,
            password: 'DemoPassword123!',
            schoolName: 'Établissement Démo',
            zone: SchoolZone::B,
            periodType: PeriodType::TRIMESTER,
        );

        self::assertSame(30, $result->createdUsers);
        self::assertSame(6, $result->createdSubjects);
        self::assertSame(3, $result->createdClasses);
        self::assertSame(12, $result->createdClassAssignments);
        self::assertSame(18, $result->createdTeacherAssignments);
        self::assertSame(18, $result->createdGuardianLinks);
        self::assertSame(18, $result->createdScheduleSlots);
        self::assertTrue($result->periodConfigurationCreated);
        self::assertTrue($result->schoolCalendarCreated);
        self::assertTrue($result->gradingConfigurationCreated);
        self::assertCount(30, $result->accounts);
        self::assertSame('2025-2026', $result->academicYearLabel);

        self::assertCount(30, $this->userRepository->findAllByTenant($this->tenantConfig->tenantId));
        self::assertCount(12, $this->userRepository->findStudentsByTenant($this->tenantConfig->tenantId));

        $student = $this->userRepository->findByEmail(
            new Email('eleve.lina.martin.demo@classeo.test'),
            $this->tenantConfig->tenantId,
        );
        self::assertNotNull($student);
        self::assertSame('hashed_DemoPassword123!', $student->hashedPassword);
        self::assertSame('DEMO-001', $student->studentNumber);

        $mathTeacher = $this->userRepository->findByEmail(
            new Email('prof.amina.benali.demo@classeo.test'),
            $this->tenantConfig->tenantId,
        );
        self::assertNotNull($mathTeacher);

        $currentYearId = $this->resolveCurrentAcademicYearId();
        self::assertCount(
            3,
            $this->classRepository->findActiveByTenantAndYear($this->tenantConfig->tenantId, $currentYearId),
        );
        self::assertCount(6, $this->subjectRepository->findActiveByTenantAndSchool(
            $this->tenantConfig->tenantId,
            \App\Administration\Domain\Model\SchoolClass\SchoolId::fromString(
                (new SchoolIdResolver())->resolveForTenant(self::TENANT_ID),
            ),
        ));
        self::assertCount(
            18,
            $this->teacherAssignmentRepository->findAllActiveByTenant($this->tenantConfig->tenantId),
        );
        self::assertCount(
            2,
            $this->studentGuardianRepository->findGuardiansForStudent($student->id, $this->tenantConfig->tenantId),
        );

        $calendar = $this->schoolCalendarRepository->findByTenantAndYear($this->tenantConfig->tenantId, $currentYearId);
        self::assertNotNull($calendar);
        self::assertCount(2, $calendar->entries());

        $periodConfiguration = $this->periodConfigurationRepository->findByAcademicYear(
            $this->tenantConfig->tenantId,
            $currentYearId,
        );
        self::assertNotNull($periodConfiguration);
        self::assertSame(PeriodType::TRIMESTER, $periodConfiguration->type);
    }

    #[Test]
    public function itIsIdempotentWhenRunTwice(): void
    {
        $this->generator->generate(
            tenantConfig: $this->tenantConfig,
            password: 'DemoPassword123!',
            schoolName: 'Établissement Démo',
            zone: SchoolZone::B,
            periodType: PeriodType::TRIMESTER,
        );

        $result = $this->generator->generate(
            tenantConfig: $this->tenantConfig,
            password: 'DemoPassword123!',
            schoolName: 'Établissement Démo',
            zone: SchoolZone::B,
            periodType: PeriodType::TRIMESTER,
        );

        self::assertSame(0, $result->createdUsers);
        self::assertSame(0, $result->createdSubjects);
        self::assertSame(0, $result->createdClasses);
        self::assertSame(0, $result->createdClassAssignments);
        self::assertSame(0, $result->createdTeacherAssignments);
        self::assertSame(0, $result->createdGuardianLinks);
        self::assertSame(0, $result->createdScheduleSlots);
        self::assertFalse($result->periodConfigurationCreated);
        self::assertFalse($result->schoolCalendarCreated);
        self::assertFalse($result->gradingConfigurationCreated);
        self::assertCount(30, $result->accounts);
    }

    #[Test]
    public function itInvalidatesPaginatedReadModelCacheAfterGeneration(): void
    {
        $loadCount = 0;

        $this->paginatedQueryCache->getOrLoad(
            'users',
            self::TENANT_ID,
            ['page' => 1, 'limit' => 30, 'role' => null, 'statut' => null, 'search' => null],
            static function () use (&$loadCount): PaginatedResult {
                ++$loadCount;

                return new PaginatedResult(
                    items: [],
                    total: 0,
                    page: 1,
                    limit: 30,
                );
            },
        );

        self::assertSame(1, $loadCount);

        $this->generator->generate(
            tenantConfig: $this->tenantConfig,
            password: 'DemoPassword123!',
            schoolName: 'Établissement Démo',
            zone: SchoolZone::B,
            periodType: PeriodType::TRIMESTER,
        );

        $this->paginatedQueryCache->getOrLoad(
            'users',
            self::TENANT_ID,
            ['page' => 1, 'limit' => 30, 'role' => null, 'statut' => null, 'search' => null],
            static function () use (&$loadCount): PaginatedResult {
                ++$loadCount;

                return new PaginatedResult(
                    items: [],
                    total: 30,
                    page: 1,
                    limit: 30,
                );
            },
        );

        self::assertSame(2, $loadCount);
    }

    private function resolveCurrentAcademicYearId(): \App\Administration\Domain\Model\SchoolClass\AcademicYearId
    {
        $tenantContext = new TenantContext();
        $tenantContext->setCurrentTenant($this->tenantConfig);
        $clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-03-10 10:00:00');
            }
        };

        $resolver = new CurrentAcademicYearResolver($tenantContext, $clock);

        return \App\Administration\Domain\Model\SchoolClass\AcademicYearId::fromString(
            $resolver->resolve('current') ?? throw new RuntimeException('Missing academic year'),
        );
    }
}
