<?php

declare(strict_types=1);

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

use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Api\Resource\TeacherAssignmentResource;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Administration\Infrastructure\Security\TeacherAssignmentVoter;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;

final class TeacherAssignmentVoterTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';

    private TeacherAssignmentVoter $voter;

    protected function setUp(): void
    {
        $this->voter = new TeacherAssignmentVoter();
    }

    #[Test]
    public function itAbstainsForUnrelatedAttributes(): void
    {
        $token = $this->tokenWithSecurityUser(Role::ADMIN->value);

        $result = $this->voter->vote($token, null, ['SOME_OTHER_ATTRIBUTE']);

        self::assertSame(Voter::ACCESS_ABSTAIN, $result);
    }

    #[Test]
    public function itDeniesAccessToUnauthenticatedUsers(): void
    {
        $token = $this->createMock(TokenInterface::class);
        $token->method('getUser')->willReturn(null);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    #[Test]
    public function itDeniesAccessToNonSecurityUserInstances(): void
    {
        $user = $this->createMock(UserInterface::class);
        $user->method('getRoles')->willReturn([Role::ADMIN->value]);

        $token = $this->createMock(TokenInterface::class);
        $token->method('getUser')->willReturn($user);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    // --- VIEW ---

    #[Test]
    #[DataProvider('viewAllowedRolesProvider')]
    public function itGrantsViewToStaffRoles(string $role): void
    {
        $token = $this->tokenWithSecurityUser($role);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    /**
     * @return iterable<string, array{string}>
     */
    public static function viewAllowedRolesProvider(): iterable
    {
        yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
        yield 'ADMIN' => [Role::ADMIN->value];
        yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value];
        yield 'SECRETARIAT' => [Role::SECRETARIAT->value];
    }

    #[Test]
    public function itGrantsViewToTeacherForOwnResource(): void
    {
        $token = $this->tokenWithSecurityUser(Role::PROF->value, self::TEACHER_ID);

        $resource = new TeacherAssignmentResource();
        $resource->teacherId = self::TEACHER_ID;

        $result = $this->voter->vote($token, $resource, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    public function itGrantsViewToTeacherForOwnDomainModel(): void
    {
        $token = $this->tokenWithSecurityUser(Role::PROF->value, self::TEACHER_ID);

        $assignment = TeacherAssignment::creer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            teacherId: UserId::fromString(self::TEACHER_ID),
            classId: ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'),
            subjectId: SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'),
            academicYearId: AcademicYearId::fromString('550e8400-e29b-41d4-a716-446655440040'),
            createdAt: new DateTimeImmutable('2026-02-10 10:00:00'),
        );

        $result = $this->voter->vote($token, $assignment, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    public function itDeniesViewToTeacherForOtherTeacherResource(): void
    {
        $token = $this->tokenWithSecurityUser(Role::PROF->value, self::TEACHER_ID);

        $resource = new TeacherAssignmentResource();
        $resource->teacherId = '550e8400-e29b-41d4-a716-446655440099';

        $result = $this->voter->vote($token, $resource, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    #[Test]
    public function itDeniesViewToTeacherWithNoSubject(): void
    {
        $token = $this->tokenWithSecurityUser(Role::PROF->value, self::TEACHER_ID);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    #[Test]
    #[DataProvider('viewDeniedRolesProvider')]
    public function itDeniesViewToNonStaffRoles(string $role): void
    {
        $token = $this->tokenWithSecurityUser($role);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::VIEW]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    /**
     * @return iterable<string, array{string}>
     */
    public static function viewDeniedRolesProvider(): iterable
    {
        yield 'PARENT' => [Role::PARENT->value];
        yield 'ELEVE' => [Role::ELEVE->value];
    }

    // --- CREATE ---

    #[Test]
    #[DataProvider('adminRolesProvider')]
    public function itGrantsCreateToAdminRoles(string $role): void
    {
        $token = $this->tokenWithSecurityUser($role);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::CREATE]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    #[DataProvider('nonAdminRolesProvider')]
    public function itDeniesCreateToNonAdminRoles(string $role): void
    {
        $token = $this->tokenWithSecurityUser($role);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::CREATE]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    // --- DELETE ---

    #[Test]
    #[DataProvider('adminRolesProvider')]
    public function itGrantsDeleteToAdminRoles(string $role): void
    {
        $token = $this->tokenWithSecurityUser($role);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::DELETE]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    #[DataProvider('nonAdminRolesProvider')]
    public function itDeniesDeleteToNonAdminRoles(string $role): void
    {
        $token = $this->tokenWithSecurityUser($role);

        $result = $this->voter->vote($token, null, [TeacherAssignmentVoter::DELETE]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    // --- Data Providers ---

    /**
     * @return iterable<string, array{string}>
     */
    public static function adminRolesProvider(): iterable
    {
        yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
        yield 'ADMIN' => [Role::ADMIN->value];
    }

    /**
     * @return iterable<string, array{string}>
     */
    public static function nonAdminRolesProvider(): iterable
    {
        yield 'PROF' => [Role::PROF->value];
        yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value];
        yield 'SECRETARIAT' => [Role::SECRETARIAT->value];
        yield 'PARENT' => [Role::PARENT->value];
        yield 'ELEVE' => [Role::ELEVE->value];
    }

    private function tokenWithSecurityUser(
        string $role,
        string $userId = '550e8400-e29b-41d4-a716-446655440001',
    ): TokenInterface {
        $securityUser = new SecurityUser(
            UserId::fromString($userId),
            'test@example.com',
            'hashed_password',
            TenantId::fromString(self::TENANT_ID),
            [$role],
        );

        $token = $this->createMock(TokenInterface::class);
        $token->method('getUser')->willReturn($securityUser);

        return $token;
    }
}
