<?php

declare(strict_types=1);

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

use App\Shared\Infrastructure\Security\TenantAwareInterface;
use App\Shared\Infrastructure\Security\TenantVoter;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use stdClass;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[CoversClass(TenantVoter::class)]
final class TenantVoterTest extends TestCase
{
    private TenantContext $tenantContext;

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

    #[Test]
    public function itAbstainsForNonTenantAwareSubjects(): void
    {
        $voter = new TenantVoter($this->tenantContext);

        $token = $this->createMock(TokenInterface::class);
        $subject = new stdClass();

        $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);

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

    #[Test]
    public function itAbstainsForNonTenantAccessAttributes(): void
    {
        $tenantIdString = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
        $this->setCurrentTenant($tenantIdString, 'ecole-alpha');

        $voter = new TenantVoter($this->tenantContext);

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

        $subject = $this->createTenantAwareSubject($tenantIdString);

        // Voter should abstain for other attributes to not bypass other voters
        foreach (['VIEW', 'EDIT', 'DELETE', 'ROLE_ADMIN'] as $attribute) {
            $result = $voter->vote($token, $subject, [$attribute]);
            self::assertSame(VoterInterface::ACCESS_ABSTAIN, $result, "Should abstain for: {$attribute}");
        }
    }

    #[Test]
    public function itDeniesAccessWhenUserNotAuthenticated(): void
    {
        $this->setCurrentTenant('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'ecole-alpha');

        $voter = new TenantVoter($this->tenantContext);

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

        $subject = $this->createTenantAwareSubject('a1b2c3d4-e5f6-7890-abcd-ef1234567890');

        $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);

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

    #[Test]
    public function itGrantsAccessWhenSubjectBelongsToCurrentTenant(): void
    {
        $tenantIdString = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
        $this->setCurrentTenant($tenantIdString, 'ecole-alpha');

        $voter = new TenantVoter($this->tenantContext);

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

        $subject = $this->createTenantAwareSubject($tenantIdString);

        $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);

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

    #[Test]
    public function itDeniesAccessWhenSubjectBelongsToDifferentTenant(): void
    {
        // Current tenant is alpha
        $this->setCurrentTenant('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'ecole-alpha');

        $voter = new TenantVoter($this->tenantContext);

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

        // Subject belongs to beta tenant
        $subject = $this->createTenantAwareSubject('b2c3d4e5-f6a7-8901-bcde-f12345678901');

        $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);

        // Should be DENIED (will be converted to 404 by access denied handler)
        self::assertSame(VoterInterface::ACCESS_DENIED, $result);
    }

    #[Test]
    public function itDeniesAccessWhenNoTenantContextSet(): void
    {
        // Don't set any tenant context
        $voter = new TenantVoter($this->tenantContext);

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

        $subject = $this->createTenantAwareSubject('a1b2c3d4-e5f6-7890-abcd-ef1234567890');

        $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);

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

    private function setCurrentTenant(string $tenantIdString, string $subdomain): void
    {
        $tenantId = TenantId::fromString($tenantIdString);
        $config = new TenantConfig(
            tenantId: $tenantId,
            subdomain: $subdomain,
            databaseUrl: "postgresql://user:pass@localhost:5432/classeo_{$subdomain}",
        );
        $this->tenantContext->setCurrentTenant($config);
    }

    private function createTenantAwareSubject(string $tenantIdString): TenantAwareInterface
    {
        $tenantId = TenantId::fromString($tenantIdString);

        $subject = $this->createMock(TenantAwareInterface::class);
        $subject->method('getTenantId')->willReturn($tenantId);

        return $subject;
    }
}
