<?php

declare(strict_types=1);

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

use App\Shared\Infrastructure\Security\TenantAccessDeniedHandler;
use App\Shared\Infrastructure\Security\TenantAwareInterface;
use App\Shared\Infrastructure\Tenant\TenantId;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use RuntimeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Throwable;

/**
 * Unit tests for TenantAccessDeniedHandler.
 *
 * CRITICAL: This handler converts 403 to 404 for cross-tenant access
 * to prevent enumeration attacks (revealing resource existence).
 */
#[CoversClass(TenantAccessDeniedHandler::class)]
final class TenantAccessDeniedHandlerTest extends TestCase
{
    private TenantAccessDeniedHandler $handler;

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

    #[Test]
    public function itConvertsAccessDeniedExceptionForTenantAwareResourceTo404(): void
    {
        // Create a TenantAware resource
        $resource = $this->createMock(TenantAwareInterface::class);
        $resource->method('getTenantId')
            ->willReturn(TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'));

        // Create AccessDeniedException with TenantAware subject
        $exception = new AccessDeniedException('Access Denied', null);
        // Note: Symfony's AccessDeniedException doesn't have a public setSubject method,
        // but it stores the subject internally. For this test, we'll use reflection.
        $reflection = new ReflectionClass($exception);
        $property = $reflection->getProperty('subject');
        $property->setAccessible(true);
        $property->setValue($exception, $resource);

        $event = $this->createExceptionEvent($exception);

        $this->handler->onKernelException($event);

        // CRITICAL: Must return 404, not 403
        self::assertTrue($event->hasResponse());
        self::assertSame(Response::HTTP_NOT_FOUND, $event->getResponse()?->getStatusCode());

        // Verify error message is generic (no information leakage)
        $content = json_decode((string) $event->getResponse()->getContent(), true);
        self::assertSame('Resource not found', $content['message']);
        self::assertSame(Response::HTTP_NOT_FOUND, $content['status']);
        self::assertSame('https://classeo.fr/errors/resource-not-found', $content['type']);

        // Must NOT reveal tenant information
        self::assertArrayNotHasKey('tenant', $content);
        self::assertArrayNotHasKey('resource', $content);
    }

    #[Test]
    public function itDoesNotModifyAccessDeniedExceptionForNonTenantAwareResources(): void
    {
        // Regular access denied (not tenant-related)
        $exception = new AccessDeniedException('Access Denied');

        $event = $this->createExceptionEvent($exception);

        $this->handler->onKernelException($event);

        // Should NOT set a response - let other handlers deal with it
        self::assertFalse($event->hasResponse());
    }

    #[Test]
    public function itIgnoresOtherExceptions(): void
    {
        // Non-AccessDeniedException
        $exception = new RuntimeException('Some error');

        $event = $this->createExceptionEvent($exception);

        $this->handler->onKernelException($event);

        // Should NOT set a response
        self::assertFalse($event->hasResponse());
    }

    #[Test]
    public function itReturns404NotRevealingResourceExistence(): void
    {
        // This test specifically validates the security requirement:
        // 403 reveals resource exists, 404 hides it

        $resource = $this->createMock(TenantAwareInterface::class);
        $resource->method('getTenantId')
            ->willReturn(TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901'));

        $exception = new AccessDeniedException('You cannot access this resource', null);
        $reflection = new ReflectionClass($exception);
        $property = $reflection->getProperty('subject');
        $property->setAccessible(true);
        $property->setValue($exception, $resource);

        $event = $this->createExceptionEvent($exception);

        $this->handler->onKernelException($event);

        $response = $event->getResponse();
        self::assertNotNull($response);

        // MUST be 404 (not 403) to prevent enumeration
        self::assertSame(404, $response->getStatusCode());
        self::assertNotSame(403, $response->getStatusCode(), 'SECURITY: Must be 404, not 403');
    }

    private function createExceptionEvent(Throwable $exception): ExceptionEvent
    {
        $kernel = $this->createMock(HttpKernelInterface::class);
        $request = Request::create('/api/resource/123');

        return new ExceptionEvent(
            $kernel,
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            $exception
        );
    }
}
