<?php

declare(strict_types=1);

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

use App\Administration\Application\Command\ImportTeachers\ImportTeachersCommand;
use App\Administration\Application\Command\ImportTeachers\ImportTeachersHandler;
use App\Administration\Domain\Event\UtilisateurInvite;
use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\ImportStatus;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\TeacherColumnMapping;
use App\Administration\Domain\Model\Import\TeacherImportBatch;
use App\Administration\Domain\Model\Import\TeacherImportField;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\ClassName;
use App\Administration\Domain\Model\SchoolClass\SchoolClass;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Domain\Model\Subject\Subject;
use App\Administration\Domain\Model\Subject\SubjectCode;
use App\Administration\Domain\Model\Subject\SubjectName;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\StatutCompte;
use App\Administration\Domain\Model\User\User;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySubjectRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryTeacherAssignmentRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryTeacherImportBatchRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Administration\Infrastructure\School\SchoolIdResolver;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;

use function count;

use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

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

    private InMemoryTeacherImportBatchRepository $importBatchRepository;
    private InMemoryUserRepository $userRepository;
    private InMemorySubjectRepository $subjectRepository;
    private InMemoryClassRepository $classRepository;
    private InMemoryTeacherAssignmentRepository $assignmentRepository;
    private ImportTeachersHandler $handler;
    private TenantId $tenantId;
    private SchoolId $schoolId;
    private MessageBusInterface $eventBus;

    protected function setUp(): void
    {
        $clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-25 10:00:00');
            }
        };

        $this->tenantId = TenantId::fromString(self::TENANT_ID);
        $schoolIdResolver = new SchoolIdResolver();
        $this->schoolId = SchoolId::fromString($schoolIdResolver->resolveForTenant(self::TENANT_ID));

        $this->importBatchRepository = new InMemoryTeacherImportBatchRepository();
        $this->userRepository = new InMemoryUserRepository();
        $this->subjectRepository = new InMemorySubjectRepository();
        $this->classRepository = new InMemoryClassRepository();
        $this->assignmentRepository = new InMemoryTeacherAssignmentRepository();

        $connection = $this->createMock(Connection::class);

        $this->eventBus = $this->createMock(MessageBusInterface::class);
        $this->eventBus->method('dispatch')->willReturnCallback(
            static fn (object $message) => new Envelope($message),
        );

        $this->handler = new ImportTeachersHandler(
            $this->importBatchRepository,
            $this->userRepository,
            $this->subjectRepository,
            $this->classRepository,
            $this->assignmentRepository,
            $schoolIdResolver,
            $connection,
            $clock,
            new NullLogger(),
            $this->eventBus,
        );
    }

    #[Test]
    public function importsTeachersWithEmailOnly(): void
    {
        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr'),
            $this->createMappedRow(2, 'Martin', 'Marie', 'marie@ecole.fr'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));

        $updatedBatch = $this->importBatchRepository->get($batch->id);

        self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
        self::assertSame(2, $updatedBatch->importedCount);
        self::assertSame(0, $updatedBatch->errorCount);
    }

    #[Test]
    public function createsTeachersWithProfRole(): void
    {
        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));

        $users = $this->userRepository->findAllByTenant($this->tenantId);

        self::assertCount(1, $users);
        self::assertTrue($users[0]->aLeRole(Role::PROF));
    }

    #[Test]
    public function assignsTeacherToSubjectsAndClasses(): void
    {
        $subject = $this->createSubject('Mathématiques', 'MATH');
        $class = $this->createClass('6A');

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr', 'Mathématiques', '6A'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));

        $users = $this->userRepository->findAllByTenant($this->tenantId);
        self::assertCount(1, $users);

        $assignments = $this->assignmentRepository->findActiveByTeacher($users[0]->id, $this->tenantId);
        self::assertCount(1, $assignments);
        self::assertTrue($assignments[0]->subjectId->equals($subject->id));
        self::assertTrue($assignments[0]->classId->equals($class->id));
    }

    #[Test]
    public function assignsMultipleSubjectsAndClasses(): void
    {
        $this->createSubject('Mathématiques', 'MATH');
        $this->createSubject('Physique', 'PHYS');
        $this->createClass('6A');
        $this->createClass('6B');

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr', 'Mathématiques, Physique', '6A, 6B'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));

        $users = $this->userRepository->findAllByTenant($this->tenantId);
        $assignments = $this->assignmentRepository->findActiveByTeacher($users[0]->id, $this->tenantId);

        // 2 subjects × 2 classes = 4 assignments
        self::assertCount(4, $assignments);
    }

    #[Test]
    public function createsMissingSubjectsWhenEnabled(): void
    {
        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr', 'Chimie'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
            createMissingSubjects: true,
        ));

        $updatedBatch = $this->importBatchRepository->get($batch->id);

        self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
        self::assertSame(1, $updatedBatch->importedCount);

        $subjects = $this->subjectRepository->findAllActiveByTenant($this->tenantId);
        self::assertCount(1, $subjects);
        self::assertSame('Chimie', (string) $subjects[0]->name);
    }

    #[Test]
    public function rejectsDuplicateEmails(): void
    {
        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr'),
            $this->createMappedRow(2, 'Martin', 'Marie', 'jean@ecole.fr'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));

        $updatedBatch = $this->importBatchRepository->get($batch->id);

        self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
        self::assertSame(1, $updatedBatch->importedCount);
        self::assertSame(1, $updatedBatch->errorCount);
    }

    #[Test]
    public function importedTeachersHaveEnAttenteStatus(): void
    {
        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));

        $users = $this->userRepository->findAllByTenant($this->tenantId);

        self::assertCount(1, $users);
        self::assertSame(StatutCompte::EN_ATTENTE, $users[0]->statut);
    }

    #[Test]
    public function importedTeachersDispatchUtilisateurInviteEvent(): void
    {
        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->expects(self::once())
            ->method('dispatch')
            ->with(self::isInstanceOf(UtilisateurInvite::class))
            ->willReturnCallback(static fn (object $message) => new Envelope($message));

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

        $schoolIdResolver = new SchoolIdResolver();
        $connection = $this->createMock(Connection::class);

        $handler = new ImportTeachersHandler(
            $this->importBatchRepository,
            $this->userRepository,
            $this->subjectRepository,
            $this->classRepository,
            $this->assignmentRepository,
            $schoolIdResolver,
            $connection,
            $clock,
            new NullLogger(),
            $eventBus,
        );

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr'),
        ]);

        ($handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));
    }

    #[Test]
    public function rejectsPreExistingEmailFromDatabase(): void
    {
        $existingUser = User::inviter(
            email: new Email('jean@ecole.fr'),
            role: Role::PROF,
            tenantId: $this->tenantId,
            schoolName: 'École Test',
            firstName: 'Existing',
            lastName: 'Teacher',
            invitedAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );
        $this->userRepository->save($existingUser);

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
        ));

        $updatedBatch = $this->importBatchRepository->get($batch->id);

        self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
        self::assertSame(0, $updatedBatch->importedCount);
        self::assertSame(1, $updatedBatch->errorCount);

        // Verify no new user was created (still only the pre-existing one)
        $users = $this->userRepository->findAllByTenant($this->tenantId);
        self::assertCount(1, $users);
    }

    #[Test]
    public function updatesExistingTeacherWhenUpdateExistingEnabled(): void
    {
        $existingUser = User::inviter(
            email: new Email('jean@ecole.fr'),
            role: Role::PROF,
            tenantId: $this->tenantId,
            schoolName: 'École Test',
            firstName: 'Ancien',
            lastName: 'Nom',
            invitedAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );
        $existingUser->pullDomainEvents();
        $this->userRepository->save($existingUser);

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
            updateExisting: true,
        ));

        $updatedBatch = $this->importBatchRepository->get($batch->id);

        self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
        self::assertSame(1, $updatedBatch->importedCount);
        self::assertSame(0, $updatedBatch->errorCount);

        $users = $this->userRepository->findAllByTenant($this->tenantId);
        self::assertCount(1, $users);
        self::assertSame('Jean', $users[0]->firstName);
        self::assertSame('Dupont', $users[0]->lastName);
    }

    #[Test]
    public function updateExistingAddsMissingAssignments(): void
    {
        $subject = $this->createSubject('Mathématiques', 'MATH');
        $class = $this->createClass('6A');

        $existingUser = User::inviter(
            email: new Email('jean@ecole.fr'),
            role: Role::PROF,
            tenantId: $this->tenantId,
            schoolName: 'École Test',
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );
        $existingUser->pullDomainEvents();
        $this->userRepository->save($existingUser);

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr', 'Mathématiques', '6A'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
            updateExisting: true,
        ));

        $assignments = $this->assignmentRepository->findActiveByTeacher($existingUser->id, $this->tenantId);
        self::assertCount(1, $assignments);
        self::assertTrue($assignments[0]->subjectId->equals($subject->id));
        self::assertTrue($assignments[0]->classId->equals($class->id));
    }

    #[Test]
    public function updateExistingDoesNotDuplicateAssignments(): void
    {
        $subject = $this->createSubject('Mathématiques', 'MATH');
        $class = $this->createClass('6A');

        $existingUser = User::inviter(
            email: new Email('jean@ecole.fr'),
            role: Role::PROF,
            tenantId: $this->tenantId,
            schoolName: 'École Test',
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );
        $existingUser->pullDomainEvents();
        $this->userRepository->save($existingUser);

        // Pre-create the assignment
        $assignment = \App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment::creer(
            tenantId: $this->tenantId,
            teacherId: $existingUser->id,
            classId: $class->id,
            subjectId: $subject->id,
            academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
            createdAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );
        $this->assignmentRepository->save($assignment);

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'Dupont', 'Jean', 'jean@ecole.fr', 'Mathématiques', '6A'),
        ]);

        ($this->handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
            updateExisting: true,
        ));

        $assignments = $this->assignmentRepository->findActiveByTeacher($existingUser->id, $this->tenantId);
        self::assertCount(1, $assignments);
    }

    #[Test]
    public function updateExistingDoesNotDispatchInvitationEvent(): void
    {
        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->expects(self::never())->method('dispatch');

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

        $schoolIdResolver = new SchoolIdResolver();
        $connection = $this->createMock(Connection::class);

        $handler = new ImportTeachersHandler(
            $this->importBatchRepository,
            $this->userRepository,
            $this->subjectRepository,
            $this->classRepository,
            $this->assignmentRepository,
            $schoolIdResolver,
            $connection,
            $clock,
            new NullLogger(),
            $eventBus,
        );

        $existingUser = User::inviter(
            email: new Email('jean@ecole.fr'),
            role: Role::PROF,
            tenantId: $this->tenantId,
            schoolName: 'École Test',
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: new DateTimeImmutable('2026-02-20 10:00:00'),
        );
        $existingUser->pullDomainEvents();
        $this->userRepository->save($existingUser);

        $batch = $this->createBatchWithRows([
            $this->createMappedRow(1, 'NouveauNom', 'NouveauPrenom', 'jean@ecole.fr'),
        ]);

        ($handler)(new ImportTeachersCommand(
            batchId: (string) $batch->id,
            tenantId: self::TENANT_ID,
            schoolName: 'École Test',
            academicYearId: self::ACADEMIC_YEAR_ID,
            updateExisting: true,
        ));
    }

    private function createSubject(string $name, string $code): Subject
    {
        $subject = Subject::creer(
            tenantId: $this->tenantId,
            schoolId: $this->schoolId,
            name: new SubjectName($name),
            code: new SubjectCode($code),
            color: null,
            createdAt: new DateTimeImmutable('2026-01-01'),
        );

        $this->subjectRepository->save($subject);

        return $subject;
    }

    private function createClass(string $name): SchoolClass
    {
        $class = SchoolClass::creer(
            tenantId: $this->tenantId,
            schoolId: $this->schoolId,
            academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
            name: new ClassName($name),
            level: null,
            capacity: null,
            createdAt: new DateTimeImmutable('2026-01-01'),
        );

        $this->classRepository->save($class);

        return $class;
    }

    /**
     * @param list<ImportRow> $rows
     */
    private function createBatchWithRows(array $rows): TeacherImportBatch
    {
        $batch = TeacherImportBatch::creer(
            tenantId: $this->tenantId,
            originalFilename: 'enseignants.csv',
            totalRows: count($rows),
            detectedColumns: ['Nom', 'Prénom', 'Email', 'Matières', 'Classes'],
            detectedFormat: KnownImportFormat::CUSTOM,
            createdAt: new DateTimeImmutable('2026-02-25 09:00:00'),
        );

        $mapping = TeacherColumnMapping::creer(
            [
                'Nom' => TeacherImportField::LAST_NAME,
                'Prénom' => TeacherImportField::FIRST_NAME,
                'Email' => TeacherImportField::EMAIL,
                'Matières' => TeacherImportField::SUBJECTS,
                'Classes' => TeacherImportField::CLASSES,
            ],
            KnownImportFormat::CUSTOM,
        );

        $batch->appliquerMapping($mapping);
        $batch->enregistrerLignes($rows);
        $this->importBatchRepository->save($batch);

        return $batch;
    }

    private function createMappedRow(
        int $line,
        string $lastName,
        string $firstName,
        string $email,
        string $subjects = '',
        string $classes = '',
    ): ImportRow {
        $mappedData = [
            'lastName' => $lastName,
            'firstName' => $firstName,
            'email' => $email,
            'subjects' => $subjects,
            'classes' => $classes,
        ];

        return new ImportRow(
            lineNumber: $line,
            rawData: $mappedData,
            mappedData: $mappedData,
        );
    }
}
