<?php

declare(strict_types=1);

namespace App\Tests\Unit\Scolarite\Infrastructure\EventHandler;

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\Port\PeriodFinder;
use App\Scolarite\Application\Port\PeriodInfo;
use App\Scolarite\Application\Service\RecalculerMoyennesService;
use App\Scolarite\Domain\Event\NotesPubliees;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Domain\Model\Grade\Grade;
use App\Scolarite\Domain\Model\Grade\GradeStatus;
use App\Scolarite\Domain\Model\Grade\GradeValue;
use App\Scolarite\Domain\Service\AverageCalculator;
use App\Scolarite\Infrastructure\EventHandler\RecalculerMoyennesOnNotesPublieesHandler;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationStatisticsRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryGradeRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryStudentAverageRepository;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId as InfraTenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class RecalculerMoyennesOnNotesPublieesHandlerTest extends TestCase
{
    private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
    public const string PERIOD_ID = '11111111-1111-1111-1111-111111111111';
    private const string STUDENT_1 = '22222222-2222-2222-2222-222222222222';
    private const string STUDENT_2 = '33333333-3333-3333-3333-333333333333';
    private const string TEACHER_ID = '44444444-4444-4444-4444-444444444444';
    private const string CLASS_ID = '55555555-5555-5555-5555-555555555555';
    private const string SUBJECT_ID = '66666666-6666-6666-6666-666666666666';

    private InMemoryEvaluationRepository $evaluationRepo;
    private InMemoryGradeRepository $gradeRepo;
    private InMemoryEvaluationStatisticsRepository $evalStatsRepo;
    private InMemoryStudentAverageRepository $studentAvgRepo;
    private RecalculerMoyennesOnNotesPublieesHandler $handler;

    protected function setUp(): void
    {
        $this->evaluationRepo = new InMemoryEvaluationRepository();
        $this->gradeRepo = new InMemoryGradeRepository();
        $this->evalStatsRepo = new InMemoryEvaluationStatisticsRepository();
        $this->studentAvgRepo = new InMemoryStudentAverageRepository();

        $tenantContext = new TenantContext();
        $tenantContext->setCurrentTenant(new TenantConfig(
            tenantId: InfraTenantId::fromString(self::TENANT_ID),
            subdomain: 'test',
            databaseUrl: 'postgresql://test',
        ));

        $periodFinder = new class implements PeriodFinder {
            public function findForDate(DateTimeImmutable $date, TenantId $tenantId): ?PeriodInfo
            {
                return new PeriodInfo(
                    periodId: RecalculerMoyennesOnNotesPublieesHandlerTest::PERIOD_ID,
                    startDate: new DateTimeImmutable('2026-01-01'),
                    endDate: new DateTimeImmutable('2026-03-31'),
                );
            }
        };

        $service = new RecalculerMoyennesService(
            evaluationRepository: $this->evaluationRepo,
            gradeRepository: $this->gradeRepo,
            evaluationStatisticsRepository: $this->evalStatsRepo,
            studentAverageRepository: $this->studentAvgRepo,
            periodFinder: $periodFinder,
            calculator: new AverageCalculator(),
        );

        $this->handler = new RecalculerMoyennesOnNotesPublieesHandler(
            tenantContext: $tenantContext,
            evaluationRepository: $this->evaluationRepo,
            gradeRepository: $this->gradeRepo,
            periodFinder: $periodFinder,
            service: $service,
        );
    }

    #[Test]
    public function itCalculatesEvaluationStatisticsOnPublication(): void
    {
        $evaluationId = $this->seedEvaluationWithGrades(
            grades: [
                [self::STUDENT_1, 14.0, GradeStatus::GRADED],
                [self::STUDENT_2, 8.0, GradeStatus::GRADED],
            ],
        );

        ($this->handler)(new NotesPubliees(
            evaluationId: $evaluationId,
            occurredOn: new DateTimeImmutable(),
        ));

        $stats = $this->evalStatsRepo->findByEvaluation($evaluationId);

        self::assertNotNull($stats);
        self::assertSame(11.0, $stats->average);
        self::assertSame(8.0, $stats->min);
        self::assertSame(14.0, $stats->max);
        self::assertSame(11.0, $stats->median);
        self::assertSame(2, $stats->gradedCount);
    }

    #[Test]
    public function itExcludesAbsentAndDispensedFromStatistics(): void
    {
        $evaluationId = $this->seedEvaluationWithGrades(
            grades: [
                [self::STUDENT_1, 16.0, GradeStatus::GRADED],
                [self::STUDENT_2, null, GradeStatus::ABSENT],
            ],
        );

        ($this->handler)(new NotesPubliees(
            evaluationId: $evaluationId,
            occurredOn: new DateTimeImmutable(),
        ));

        $stats = $this->evalStatsRepo->findByEvaluation($evaluationId);

        self::assertNotNull($stats);
        self::assertSame(16.0, $stats->average);
        self::assertSame(1, $stats->gradedCount);
    }

    #[Test]
    public function itCalculatesSubjectAverageForEachStudent(): void
    {
        $evaluationId = $this->seedEvaluationWithGrades(
            grades: [
                [self::STUDENT_1, 15.0, GradeStatus::GRADED],
                [self::STUDENT_2, 10.0, GradeStatus::GRADED],
            ],
        );

        ($this->handler)(new NotesPubliees(
            evaluationId: $evaluationId,
            occurredOn: new DateTimeImmutable(),
        ));

        $student1Avg = $this->studentAvgRepo->findSubjectAverage(
            UserId::fromString(self::STUDENT_1),
            SubjectId::fromString(self::SUBJECT_ID),
            self::PERIOD_ID,
            TenantId::fromString(self::TENANT_ID),
        );

        self::assertNotNull($student1Avg);
        self::assertSame(15.0, $student1Avg['average']);
        self::assertSame(1, $student1Avg['gradeCount']);

        $student2Avg = $this->studentAvgRepo->findSubjectAverage(
            UserId::fromString(self::STUDENT_2),
            SubjectId::fromString(self::SUBJECT_ID),
            self::PERIOD_ID,
            TenantId::fromString(self::TENANT_ID),
        );

        self::assertNotNull($student2Avg);
        self::assertSame(10.0, $student2Avg['average']);
    }

    #[Test]
    public function itCalculatesWeightedSubjectAverageAcrossMultipleEvaluations(): void
    {
        // Première évaluation publiée (coef 2)
        $eval1Id = $this->seedEvaluationWithGrades(
            grades: [[self::STUDENT_1, 16.0, GradeStatus::GRADED]],
            coefficient: 2.0,
            published: true,
        );

        // Publier la première évaluation d'abord
        ($this->handler)(new NotesPubliees(
            evaluationId: $eval1Id,
            occurredOn: new DateTimeImmutable(),
        ));

        // Deuxième évaluation publiée (coef 1)
        $eval2Id = $this->seedEvaluationWithGrades(
            grades: [[self::STUDENT_1, 10.0, GradeStatus::GRADED]],
            coefficient: 1.0,
            published: true,
        );

        ($this->handler)(new NotesPubliees(
            evaluationId: $eval2Id,
            occurredOn: new DateTimeImmutable(),
        ));

        $student1Avg = $this->studentAvgRepo->findSubjectAverage(
            UserId::fromString(self::STUDENT_1),
            SubjectId::fromString(self::SUBJECT_ID),
            self::PERIOD_ID,
            TenantId::fromString(self::TENANT_ID),
        );

        self::assertNotNull($student1Avg);
        // (16×2 + 10×1) / (2+1) = 42/3 = 14.0
        self::assertSame(14.0, $student1Avg['average']);
        self::assertSame(2, $student1Avg['gradeCount']);
    }

    #[Test]
    public function itCalculatesGeneralAverage(): void
    {
        $evaluationId = $this->seedEvaluationWithGrades(
            grades: [[self::STUDENT_1, 14.0, GradeStatus::GRADED]],
        );

        ($this->handler)(new NotesPubliees(
            evaluationId: $evaluationId,
            occurredOn: new DateTimeImmutable(),
        ));

        $generalAvg = $this->studentAvgRepo->findGeneralAverageForStudent(
            UserId::fromString(self::STUDENT_1),
            self::PERIOD_ID,
            TenantId::fromString(self::TENANT_ID),
        );

        self::assertSame(14.0, $generalAvg);
    }

    #[Test]
    public function itDoesNothingWhenEvaluationNotFound(): void
    {
        $unknownId = EvaluationId::generate();

        ($this->handler)(new NotesPubliees(
            evaluationId: $unknownId,
            occurredOn: new DateTimeImmutable(),
        ));

        self::assertNull($this->evalStatsRepo->findByEvaluation($unknownId));
    }

    /**
     * @param list<array{0: string, 1: float|null, 2: GradeStatus}> $grades
     */
    private function seedEvaluationWithGrades(
        array $grades,
        float $coefficient = 1.0,
        bool $published = true,
    ): EvaluationId {
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $now = new DateTimeImmutable();

        $evaluation = Evaluation::creer(
            tenantId: $tenantId,
            classId: ClassId::fromString(self::CLASS_ID),
            subjectId: SubjectId::fromString(self::SUBJECT_ID),
            teacherId: UserId::fromString(self::TEACHER_ID),
            title: 'Test Evaluation',
            description: null,
            evaluationDate: new DateTimeImmutable('2026-02-15'),
            gradeScale: new GradeScale(20),
            coefficient: new Coefficient($coefficient),
            now: $now,
        );

        if ($published) {
            $evaluation->publierNotes($now);
        }

        $evaluation->pullDomainEvents();
        $this->evaluationRepo->save($evaluation);

        foreach ($grades as [$studentId, $value, $status]) {
            $grade = Grade::saisir(
                tenantId: $tenantId,
                evaluationId: $evaluation->id,
                studentId: UserId::fromString($studentId),
                value: $value !== null ? new GradeValue($value) : null,
                status: $status,
                gradeScale: new GradeScale(20),
                createdBy: UserId::fromString(self::TEACHER_ID),
                now: $now,
            );
            $grade->pullDomainEvents();
            $this->gradeRepo->save($grade);
        }

        return $evaluation->id;
    }
}
