<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Application\Command\ConfigureGradingMode;

use App\Administration\Application\Command\ConfigureGradingMode\ConfigureGradingModeCommand;
use App\Administration\Application\Command\ConfigureGradingMode\ConfigureGradingModeHandler;
use App\Administration\Application\Port\GradeExistenceChecker;
use App\Administration\Domain\Exception\CannotChangeGradingModeWithExistingGradesException;
use App\Administration\Domain\Model\GradingConfiguration\GradingMode;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryGradingConfigurationRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Override;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class ConfigureGradingModeHandlerTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440003';

    private InMemoryGradingConfigurationRepository $repository;
    private Clock $clock;

    protected function setUp(): void
    {
        $this->repository = new InMemoryGradingConfigurationRepository();
        $this->clock = new class implements Clock {
            #[Override]
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-01 10:00:00');
            }
        };
    }

    #[Test]
    public function itCreatesNewConfigurationWhenNoneExists(): void
    {
        $handler = $this->createHandler(hasGrades: false);

        $result = $handler(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'numeric_20',
        ));

        self::assertSame(GradingMode::NUMERIC_20, $result->gradingConfiguration->mode);
    }

    #[Test]
    public function itPersistsConfigurationInRepository(): void
    {
        $handler = $this->createHandler(hasGrades: false);

        $handler(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'competencies',
        ));

        $found = $this->repository->findBySchoolAndYear(
            TenantId::fromString(self::TENANT_ID),
            SchoolId::fromString(self::SCHOOL_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );

        self::assertNotNull($found);
        self::assertSame(GradingMode::COMPETENCIES, $found->gradingConfiguration->mode);
    }

    #[Test]
    public function itChangesExistingModeWhenNoGradesExist(): void
    {
        $handler = $this->createHandler(hasGrades: false);

        $handler(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'numeric_20',
        ));

        $result = $handler(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'letters',
        ));

        self::assertSame(GradingMode::LETTERS, $result->gradingConfiguration->mode);
    }

    #[Test]
    public function itBlocksChangeWhenGradesExist(): void
    {
        $handlerNoGrades = $this->createHandler(hasGrades: false);
        $handlerNoGrades(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'numeric_20',
        ));

        $handlerWithGrades = $this->createHandler(hasGrades: true);

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

        $handlerWithGrades(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'competencies',
        ));
    }

    #[Test]
    public function itAllowsSameModeEvenWithGradesExisting(): void
    {
        $handlerNoGrades = $this->createHandler(hasGrades: false);
        $handlerNoGrades(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'numeric_20',
        ));

        $handlerWithGrades = $this->createHandler(hasGrades: true);
        $result = $handlerWithGrades(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'numeric_20',
        ));

        self::assertSame(GradingMode::NUMERIC_20, $result->gradingConfiguration->mode);
    }

    #[Test]
    public function itBlocksInitialNonDefaultModeWhenGradesExist(): void
    {
        $handler = $this->createHandler(hasGrades: true);

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

        $handler(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'competencies',
        ));
    }

    #[Test]
    public function itIsolatesConfigurationByTenant(): void
    {
        $handler = $this->createHandler(hasGrades: false);

        $handler(new ConfigureGradingModeCommand(
            tenantId: self::TENANT_ID,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'numeric_20',
        ));

        $otherTenantId = '550e8400-e29b-41d4-a716-446655440099';
        $handler(new ConfigureGradingModeCommand(
            tenantId: $otherTenantId,
            schoolId: self::SCHOOL_ID,
            academicYearId: self::ACADEMIC_YEAR_ID,
            gradingMode: 'competencies',
        ));

        $config1 = $this->repository->findBySchoolAndYear(
            TenantId::fromString(self::TENANT_ID),
            SchoolId::fromString(self::SCHOOL_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );

        $config2 = $this->repository->findBySchoolAndYear(
            TenantId::fromString($otherTenantId),
            SchoolId::fromString(self::SCHOOL_ID),
            AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
        );

        self::assertNotNull($config1);
        self::assertNotNull($config2);
        self::assertSame(GradingMode::NUMERIC_20, $config1->gradingConfiguration->mode);
        self::assertSame(GradingMode::COMPETENCIES, $config2->gradingConfiguration->mode);
    }

    private function createHandler(bool $hasGrades): ConfigureGradingModeHandler
    {
        $gradeChecker = new class($hasGrades) implements GradeExistenceChecker {
            public function __construct(private bool $hasGrades)
            {
            }

            #[Override]
            public function hasGradesInPeriod(TenantId $tenantId, AcademicYearId $academicYearId, int $periodSequence): bool
            {
                return $this->hasGrades;
            }

            #[Override]
            public function hasGradesForYear(TenantId $tenantId, SchoolId $schoolId, AcademicYearId $academicYearId): bool
            {
                return $this->hasGrades;
            }
        };

        return new ConfigureGradingModeHandler(
            $this->repository,
            $gradeChecker,
            $this->clock,
        );
    }
}
