<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Application\Query\GetAllAssignments;

use App\Administration\Application\Query\GetAllAssignments\GetAllAssignmentsHandler;
use App\Administration\Application\Query\GetAllAssignments\GetAllAssignmentsQuery;
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\SchoolClass\SchoolLevel;
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\TeacherAssignment\TeacherAssignment;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
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\InMemoryUserRepository;
use App\Shared\Domain\Tenant\TenantId;

use function count;

use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;

final class GetAllAssignmentsHandlerTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string OTHER_TENANT_ID = '550e8400-e29b-41d4-a716-446655440099';

    private InMemoryTeacherAssignmentRepository $assignmentRepository;
    private InMemoryUserRepository $userRepository;
    private InMemoryClassRepository $classRepository;
    private InMemorySubjectRepository $subjectRepository;
    private GetAllAssignmentsHandler $handler;

    protected function setUp(): void
    {
        $this->assignmentRepository = new InMemoryTeacherAssignmentRepository();
        $this->userRepository = new InMemoryUserRepository();
        $this->classRepository = new InMemoryClassRepository();
        $this->subjectRepository = new InMemorySubjectRepository();
        $this->handler = new GetAllAssignmentsHandler(
            $this->assignmentRepository,
            $this->userRepository,
            $this->classRepository,
            $this->subjectRepository,
            new NullLogger(),
        );
    }

    #[Test]
    public function returnsAllActiveAssignmentsWithNames(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(tenantId: self::TENANT_ID);
        $result = ($this->handler)($query);

        self::assertCount(2, $result->items);
        self::assertSame(2, $result->total);
        self::assertSame(1, $result->page);
        self::assertSame(30, $result->limit);

        // Verify denormalized names are populated
        $dto = $result->items[0];
        self::assertNotSame('', $dto->teacherFirstName);
        self::assertNotSame('', $dto->className);
        self::assertNotSame('', $dto->subjectName);
    }

    #[Test]
    public function searchesByTeacherName(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            search: 'Jean',
        );
        $result = ($this->handler)($query);

        self::assertCount(1, $result->items);
        self::assertSame('Jean', $result->items[0]->teacherFirstName);
    }

    #[Test]
    public function searchesByClassName(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            search: '6ème',
        );
        $result = ($this->handler)($query);

        self::assertGreaterThanOrEqual(1, count($result->items));
        self::assertStringContainsString('6ème', $result->items[0]->className);
    }

    #[Test]
    public function searchesBySubjectName(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            search: 'Français',
        );
        $result = ($this->handler)($query);

        self::assertCount(1, $result->items);
        self::assertSame('Français', $result->items[0]->subjectName);
    }

    #[Test]
    public function searchIsCaseInsensitive(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            search: 'jean',
        );
        $result = ($this->handler)($query);

        self::assertCount(1, $result->items);
        self::assertSame('Jean', $result->items[0]->teacherFirstName);
    }

    #[Test]
    public function paginatesResults(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            page: 1,
            limit: 1,
        );
        $result = ($this->handler)($query);

        self::assertCount(1, $result->items);
        self::assertSame(2, $result->total);
        self::assertSame(1, $result->page);
        self::assertSame(1, $result->limit);
        self::assertSame(2, $result->totalPages());
    }

    #[Test]
    public function returnsSecondPage(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            page: 2,
            limit: 1,
        );
        $result = ($this->handler)($query);

        self::assertCount(1, $result->items);
        self::assertSame(2, $result->total);
        self::assertSame(2, $result->page);
    }

    #[Test]
    public function returnsEmptyWhenNoMatches(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            search: 'nonexistent',
        );
        $result = ($this->handler)($query);

        self::assertCount(0, $result->items);
        self::assertSame(0, $result->total);
    }

    #[Test]
    public function excludesAssignmentsFromOtherTenants(): void
    {
        $this->seedData();

        $query = new GetAllAssignmentsQuery(tenantId: self::OTHER_TENANT_ID);
        $result = ($this->handler)($query);

        self::assertCount(0, $result->items);
        self::assertSame(0, $result->total);
    }

    #[Test]
    public function handlesOrphanedAssignment(): void
    {
        // Create an assignment with no matching teacher/class/subject
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $now = new DateTimeImmutable('2026-02-01 10:00:00');

        $orphanedClass = SchoolClass::creer(
            tenantId: $tenantId,
            schoolId: SchoolId::generate(),
            academicYearId: AcademicYearId::generate(),
            name: new ClassName('Orphan Class'),
            level: null,
            capacity: null,
            createdAt: $now,
        );
        $this->classRepository->save($orphanedClass);

        $orphanedSubject = Subject::creer(
            tenantId: $tenantId,
            schoolId: SchoolId::generate(),
            name: new SubjectName('Orphan Subject'),
            code: new SubjectCode('ORPH'),
            color: null,
            createdAt: $now,
        );
        $this->subjectRepository->save($orphanedSubject);

        // Teacher does NOT exist in userRepository
        $orphanedTeacher = User::inviter(
            email: new Email('orphan@example.com'),
            role: Role::PROF,
            tenantId: TenantId::fromString(self::OTHER_TENANT_ID), // Different tenant — won't be found
            schoolName: 'Autre',
            firstName: 'Ghost',
            lastName: 'Teacher',
            invitedAt: $now,
        );
        $this->userRepository->save($orphanedTeacher);

        $assignment = TeacherAssignment::creer(
            tenantId: $tenantId,
            teacherId: $orphanedTeacher->id,
            classId: $orphanedClass->id,
            subjectId: $orphanedSubject->id,
            academicYearId: AcademicYearId::generate(),
            createdAt: $now,
        );
        $this->assignmentRepository->save($assignment);

        $query = new GetAllAssignmentsQuery(tenantId: self::TENANT_ID);
        $result = ($this->handler)($query);

        // Assignment should appear with empty teacher names (orphaned reference logged as warning)
        self::assertCount(1, $result->items);
        self::assertSame('', $result->items[0]->teacherFirstName);
        self::assertSame('', $result->items[0]->teacherLastName);
    }

    #[Test]
    public function clampsInvalidPageToOne(): void
    {
        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            page: -1,
        );

        self::assertSame(1, $query->page);
    }

    #[Test]
    public function clampsExcessiveLimitToMax(): void
    {
        $query = new GetAllAssignmentsQuery(
            tenantId: self::TENANT_ID,
            limit: 999,
        );

        self::assertSame(100, $query->limit);
    }

    private function seedData(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $schoolId = SchoolId::generate();
        $academicYearId = AcademicYearId::generate();
        $now = new DateTimeImmutable('2026-02-01 10:00:00');

        // Create teachers
        $teacher1 = User::inviter(
            email: new Email('teacher1@example.com'),
            role: Role::PROF,
            tenantId: $tenantId,
            schoolName: 'École Alpha',
            firstName: 'Jean',
            lastName: 'Dupont',
            invitedAt: $now,
        );
        $this->userRepository->save($teacher1);

        $teacher2 = User::inviter(
            email: new Email('teacher2@example.com'),
            role: Role::PROF,
            tenantId: $tenantId,
            schoolName: 'École Alpha',
            firstName: 'Marie',
            lastName: 'Martin',
            invitedAt: $now,
        );
        $this->userRepository->save($teacher2);

        // Create classes
        $class1 = SchoolClass::creer(
            tenantId: $tenantId,
            schoolId: $schoolId,
            academicYearId: $academicYearId,
            name: new ClassName('6ème A'),
            level: SchoolLevel::SIXIEME,
            capacity: 30,
            createdAt: $now,
        );
        $this->classRepository->save($class1);

        // Create subjects
        $subject1 = Subject::creer(
            tenantId: $tenantId,
            schoolId: $schoolId,
            name: new SubjectName('Mathématiques'),
            code: new SubjectCode('MATH'),
            color: null,
            createdAt: $now,
        );
        $this->subjectRepository->save($subject1);

        $subject2 = Subject::creer(
            tenantId: $tenantId,
            schoolId: $schoolId,
            name: new SubjectName('Français'),
            code: new SubjectCode('FR'),
            color: null,
            createdAt: $now,
        );
        $this->subjectRepository->save($subject2);

        // Create assignments
        $assignment1 = TeacherAssignment::creer(
            tenantId: $tenantId,
            teacherId: $teacher1->id,
            classId: $class1->id,
            subjectId: $subject1->id,
            academicYearId: $academicYearId,
            createdAt: $now,
        );
        $this->assignmentRepository->save($assignment1);

        $assignment2 = TeacherAssignment::creer(
            tenantId: $tenantId,
            teacherId: $teacher2->id,
            classId: $class1->id,
            subjectId: $subject2->id,
            academicYearId: $academicYearId,
            createdAt: $now,
        );
        $this->assignmentRepository->save($assignment2);
    }
}
