<?php

declare(strict_types=1);

namespace App\Tests\Integration\Shared\Infrastructure\Tenant;

use App\Shared\Infrastructure\Security\TenantAwareInterface;
use App\Shared\Infrastructure\Security\TenantVoter;
use App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantMiddleware;
use App\Shared\Infrastructure\Tenant\TenantResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Cross-tenant isolation tests verifying that:
 * 1. Users from tenant A cannot see data from tenant B
 * 2. Subdomain mismatch results in rejection
 * 3. Unknown subdomains return 404
 * 4. Resource access for other tenants returns 404 (not 403)
 */
#[CoversClass(TenantMiddleware::class)]
#[CoversClass(TenantVoter::class)]
#[CoversClass(TenantResolver::class)]
final class CrossTenantIsolationTest extends TestCase
{
    private const string BASE_DOMAIN = 'classeo.local';

    private TenantId $tenantIdAlpha;
    private TenantId $tenantIdBeta;
    private TenantConfig $configAlpha;
    private TenantConfig $configBeta;
    private InMemoryTenantRegistry $registry;
    private TenantContext $context;
    private TenantResolver $resolver;
    private TenantMiddleware $middleware;
    private TenantVoter $voter;

    protected function setUp(): void
    {
        $this->tenantIdAlpha = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
        $this->tenantIdBeta = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');

        $this->configAlpha = new TenantConfig(
            tenantId: $this->tenantIdAlpha,
            subdomain: 'ecole-alpha',
            databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
        );
        $this->configBeta = new TenantConfig(
            tenantId: $this->tenantIdBeta,
            subdomain: 'ecole-beta',
            databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_beta',
        );

        $this->registry = new InMemoryTenantRegistry([$this->configAlpha, $this->configBeta]);
        $this->context = new TenantContext();
        $this->resolver = new TenantResolver($this->registry, self::BASE_DOMAIN);
        $this->middleware = new TenantMiddleware($this->resolver, $this->context);
        $this->voter = new TenantVoter($this->context);
    }

    #[Test]
    public function userFromTenantACannotAccessResourceFromTenantB(): void
    {
        // User authenticates on tenant Alpha
        $this->context->setCurrentTenant($this->configAlpha);

        // Create a resource belonging to tenant Beta
        $resourceFromBeta = $this->createTenantAwareResource($this->tenantIdBeta);

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

        // Vote should DENY access
        $result = $this->voter->vote($token, $resourceFromBeta, [TenantVoter::ATTRIBUTE]);

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

    #[Test]
    public function userFromTenantACanAccessOwnResources(): void
    {
        // User authenticates on tenant Alpha
        $this->context->setCurrentTenant($this->configAlpha);

        // Create a resource belonging to tenant Alpha
        $resourceFromAlpha = $this->createTenantAwareResource($this->tenantIdAlpha);

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

        // Vote should GRANT access
        $result = $this->voter->vote($token, $resourceFromAlpha, [TenantVoter::ATTRIBUTE]);

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

    #[Test]
    public function unknownSubdomainReturns404(): void
    {
        $request = Request::create('https://ecole-inexistant.classeo.local/api/dashboard');
        $event = $this->createRequestEvent($request);

        $this->middleware->onKernelRequest($event);

        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::assertArrayNotHasKey('subdomain', $content);
        self::assertArrayNotHasKey('tenant', $content);
    }

    #[Test]
    public function validSubdomainSetsContext(): void
    {
        $request = Request::create('https://ecole-alpha.classeo.local/api/dashboard');
        $event = $this->createRequestEvent($request);

        $this->middleware->onKernelRequest($event);

        self::assertFalse($event->hasResponse()); // No error response
        self::assertTrue($this->context->hasTenant());
        self::assertTrue($this->tenantIdAlpha->equals($this->context->getCurrentTenantId()));
    }

    #[Test]
    public function accessToResourceFromOtherTenantReturns404NotRevealingExistence(): void
    {
        // This test verifies the critical security requirement:
        // When denied access to a resource from another tenant,
        // the response MUST be 404 (not 403) to prevent enumeration attacks.

        // User is authenticated on tenant Alpha
        $this->context->setCurrentTenant($this->configAlpha);

        // Resource exists in tenant Beta
        $resourceFromBeta = $this->createTenantAwareResource($this->tenantIdBeta);

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

        // Vote should DENY (which will be converted to 404 by TenantAccessDeniedHandler)
        $result = $this->voter->vote($token, $resourceFromBeta, [TenantVoter::ATTRIBUTE]);

        // The voter returns DENIED, not ABSTAIN
        // This ensures the access is actively denied rather than just not supported
        self::assertSame(VoterInterface::ACCESS_DENIED, $result);

        // The AccessDeniedHandler will convert this to 404
        // (tested separately in TenantAccessDeniedHandler tests)
    }

    #[Test]
    public function tenantContextIsClearedOnRequestTermination(): void
    {
        // Set a tenant
        $this->context->setCurrentTenant($this->configAlpha);
        self::assertTrue($this->context->hasTenant());

        // Terminate request
        $this->middleware->onKernelTerminate();

        // Context should be cleared
        self::assertFalse($this->context->hasTenant());
    }

    #[Test]
    public function subdomainMismatchWithAuthenticatedUserFromDifferentTenant(): void
    {
        // User tries to access ecole-beta.classeo.local
        $request = Request::create('https://ecole-beta.classeo.local/api/dashboard');
        $event = $this->createRequestEvent($request);

        $this->middleware->onKernelRequest($event);

        // Context should be set to Beta (the subdomain in the request)
        self::assertTrue($this->context->hasTenant());
        self::assertTrue($this->tenantIdBeta->equals($this->context->getCurrentTenantId()));

        // If a user's JWT was issued by Alpha but they're accessing Beta,
        // the authentication layer should reject the token (not tested here,
        // that's handled by JWT validation which checks tenant claims)
    }

    #[Test]
    public function reservedSubdomainsAreRejected(): void
    {
        $reservedSubdomains = ['www', 'api', 'admin', 'static', 'cdn', 'mail'];

        foreach ($reservedSubdomains as $subdomain) {
            $this->context->clear();

            $request = Request::create("https://{$subdomain}.classeo.local/api/test");
            $event = $this->createRequestEvent($request);

            $this->middleware->onKernelRequest($event);

            self::assertTrue(
                $event->hasResponse(),
                "Expected 404 response for reserved subdomain: {$subdomain}"
            );
            self::assertSame(
                Response::HTTP_NOT_FOUND,
                $event->getResponse()?->getStatusCode(),
                "Expected 404 for reserved subdomain: {$subdomain}"
            );
        }
    }

    #[Test]
    public function testWithTwoTenantsMinimum(): void
    {
        // This test ensures we always test with at least 2 tenants
        // as per the project's critical rules

        // Verify both tenants are set up
        self::assertTrue($this->registry->exists('ecole-alpha'));
        self::assertTrue($this->registry->exists('ecole-beta'));

        // Test Alpha isolation
        $this->context->setCurrentTenant($this->configAlpha);
        $resourceAlpha = $this->createTenantAwareResource($this->tenantIdAlpha);
        $resourceBeta = $this->createTenantAwareResource($this->tenantIdBeta);

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

        self::assertSame(
            VoterInterface::ACCESS_GRANTED,
            $this->voter->vote($token, $resourceAlpha, [TenantVoter::ATTRIBUTE]),
            'Alpha user should access Alpha resource'
        );
        self::assertSame(
            VoterInterface::ACCESS_DENIED,
            $this->voter->vote($token, $resourceBeta, [TenantVoter::ATTRIBUTE]),
            'Alpha user should NOT access Beta resource'
        );

        // Test Beta isolation
        $this->context->setCurrentTenant($this->configBeta);

        self::assertSame(
            VoterInterface::ACCESS_DENIED,
            $this->voter->vote($token, $resourceAlpha, [TenantVoter::ATTRIBUTE]),
            'Beta user should NOT access Alpha resource'
        );
        self::assertSame(
            VoterInterface::ACCESS_GRANTED,
            $this->voter->vote($token, $resourceBeta, [TenantVoter::ATTRIBUTE]),
            'Beta user should access Beta resource'
        );
    }

    private function createRequestEvent(Request $request): RequestEvent
    {
        $kernel = $this->createMock(HttpKernelInterface::class);

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

    private function createTenantAwareResource(TenantId $tenantId): TenantAwareInterface
    {
        $resource = $this->createMock(TenantAwareInterface::class);
        $resource->method('getTenantId')->willReturn($tenantId);

        return $resource;
    }
}
