<?php

declare(strict_types=1);

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

use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Administration\Infrastructure\Security\StudentGuardianVoter;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId as InfraTenantId;
use DateTimeImmutable;
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 StudentGuardianVoterTest extends TestCase
{
    private InMemoryStudentGuardianRepository $repository;
    private TenantContext $tenantContext;
    private StudentGuardianVoter $voter;
    private TenantId $tenantId;

    protected function setUp(): void
    {
        $this->repository = new InMemoryStudentGuardianRepository();
        $this->tenantContext = new TenantContext();
        $this->tenantId = TenantId::generate();
        $this->tenantContext->setCurrentTenant(new TenantConfig(
            tenantId: InfraTenantId::fromString((string) $this->tenantId),
            subdomain: 'test',
            databaseUrl: 'sqlite:///:memory:',
        ));
        $this->voter = new StudentGuardianVoter($this->repository, $this->tenantContext);
    }

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

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

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

    #[Test]
    public function itAbstainsWhenSubjectIsNotAString(): void
    {
        $token = $this->tokenWithSecurityUser('ROLE_ADMIN');

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

        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, 'some-student-id', [StudentGuardianVoter::VIEW_STUDENT]);

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

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

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

        $result = $this->voter->vote($token, 'some-student-id', [StudentGuardianVoter::VIEW_STUDENT]);

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

    #[Test]
    public function itGrantsViewToSuperAdmin(): void
    {
        $result = $this->voteWithRole('ROLE_SUPER_ADMIN');
        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    public function itGrantsViewToAdmin(): void
    {
        $result = $this->voteWithRole('ROLE_ADMIN');
        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    public function itGrantsViewToSecretariat(): void
    {
        $result = $this->voteWithRole('ROLE_SECRETARIAT');
        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    public function itGrantsViewToProf(): void
    {
        $result = $this->voteWithRole('ROLE_PROF');
        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    public function itGrantsViewToVieScolaire(): void
    {
        $result = $this->voteWithRole('ROLE_VIE_SCOLAIRE');
        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    public function itGrantsViewToLinkedParent(): void
    {
        $parentId = UserId::generate();
        $studentId = UserId::generate();

        $link = StudentGuardian::lier(
            studentId: $studentId,
            guardianId: $parentId,
            relationshipType: RelationshipType::MOTHER,
            tenantId: $this->tenantId,
            createdAt: new DateTimeImmutable(),
        );
        $this->repository->save($link);

        $token = $this->tokenWithSecurityUser('ROLE_PARENT', $parentId);

        $result = $this->voter->vote($token, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]);

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

    #[Test]
    public function itDeniesViewToUnlinkedParent(): void
    {
        $parentId = UserId::generate();
        $otherStudentId = UserId::generate();

        $token = $this->tokenWithSecurityUser('ROLE_PARENT', $parentId);

        $result = $this->voter->vote($token, (string) $otherStudentId, [StudentGuardianVoter::VIEW_STUDENT]);

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

    #[Test]
    public function itDeniesViewToEleve(): void
    {
        $result = $this->voteWithRole('ROLE_ELEVE');
        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    #[Test]
    public function itGrantsViewToEachSeparatedParent(): void
    {
        $parent1Id = UserId::generate();
        $parent2Id = UserId::generate();
        $studentId = UserId::generate();

        $link1 = StudentGuardian::lier(
            studentId: $studentId,
            guardianId: $parent1Id,
            relationshipType: RelationshipType::FATHER,
            tenantId: $this->tenantId,
            createdAt: new DateTimeImmutable(),
        );
        $link2 = StudentGuardian::lier(
            studentId: $studentId,
            guardianId: $parent2Id,
            relationshipType: RelationshipType::MOTHER,
            tenantId: $this->tenantId,
            createdAt: new DateTimeImmutable(),
        );
        $this->repository->save($link1);
        $this->repository->save($link2);

        $token1 = $this->tokenWithSecurityUser('ROLE_PARENT', $parent1Id);
        $token2 = $this->tokenWithSecurityUser('ROLE_PARENT', $parent2Id);

        self::assertSame(Voter::ACCESS_GRANTED, $this->voter->vote($token1, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]));
        self::assertSame(Voter::ACCESS_GRANTED, $this->voter->vote($token2, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]));
    }

    #[Test]
    public function itDeniesParentWhenNoTenantSet(): void
    {
        $parentId = UserId::generate();
        $studentId = UserId::generate();

        $tenantContext = new TenantContext();
        $voter = new StudentGuardianVoter($this->repository, $tenantContext);

        $token = $this->tokenWithSecurityUser('ROLE_PARENT', $parentId);

        $result = $voter->vote($token, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]);

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

    #[Test]
    public function itGrantsManageToAdmin(): void
    {
        $token = $this->tokenWithSecurityUser('ROLE_ADMIN');

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

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

    #[Test]
    public function itDeniesManageToParent(): void
    {
        $token = $this->tokenWithSecurityUser('ROLE_PARENT');

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

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

    #[Test]
    public function itDeniesManageToEleve(): void
    {
        $token = $this->tokenWithSecurityUser('ROLE_ELEVE');

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

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

    private function voteWithRole(string $role): int
    {
        $token = $this->tokenWithSecurityUser($role);

        return $this->voter->vote($token, (string) UserId::generate(), [StudentGuardianVoter::VIEW_STUDENT]);
    }

    private function tokenWithSecurityUser(string $role, ?UserId $userId = null): TokenInterface
    {
        $securityUser = new SecurityUser(
            userId: $userId ?? UserId::generate(),
            email: 'test@example.com',
            hashedPassword: 'hashed',
            tenantId: $this->tenantId,
            roles: [$role],
        );

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

        return $token;
    }
}
