<?php

declare(strict_types=1);

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

use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Domain\Model\TeacherReplacement\ClassSubjectPair;
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacement;
use App\Scolarite\Infrastructure\Security\TeacherReplacementVoter;
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 TeacherReplacementVoterTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string REPLACED_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
    private const string REPLACEMENT_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440011';

    private TeacherReplacementVoter $voter;

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

    #[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, [TeacherReplacementVoter::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, [TeacherReplacementVoter::VIEW]);

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

    // --- VIEW ---

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

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

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

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

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

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

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

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

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

    #[Test]
    public function itDeniesViewToUninvolvedTeacher(): void
    {
        $token = $this->tokenWithSecurityUser(Role::PROF->value, '550e8400-e29b-41d4-a716-446655440099');
        $replacement = $this->createReplacement();

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

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

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

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

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

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

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

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

    // --- CREATE ---

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

        $result = $this->voter->vote($token, null, [TeacherReplacementVoter::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, [TeacherReplacementVoter::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, [TeacherReplacementVoter::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, [TeacherReplacementVoter::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];
    }

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

    private function createReplacement(): TeacherReplacement
    {
        return TeacherReplacement::designer(
            tenantId: TenantId::fromString(self::TENANT_ID),
            replacedTeacherId: UserId::fromString(self::REPLACED_TEACHER_ID),
            replacementTeacherId: UserId::fromString(self::REPLACEMENT_TEACHER_ID),
            startDate: new DateTimeImmutable('2026-03-01'),
            endDate: new DateTimeImmutable('2026-03-31'),
            classes: [
                new ClassSubjectPair(
                    ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'),
                    SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'),
                ),
            ],
            reason: null,
            createdBy: UserId::fromString('550e8400-e29b-41d4-a716-446655440099'),
            now: new DateTimeImmutable('2026-02-15 10:00:00'),
        );
    }

    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;
    }
}
