<?php

declare(strict_types=1);

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

use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantEntityManagerFactory;
use App\Shared\Infrastructure\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantRegistry;
use DateTimeImmutable;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
 * Unit tests for TenantEntityManagerFactory.
 *
 * Note: These tests use SQLite in-memory databases which requires proper
 * Doctrine ORM configuration. For full integration testing with PostgreSQL,
 * see the Integration tests.
 */
#[CoversClass(TenantEntityManagerFactory::class)]
final class TenantEntityManagerFactoryTest extends TestCase
{
    private TenantRegistry $registry;
    private Clock $clock;
    private Configuration $ormConfiguration;

    protected function setUp(): void
    {
        $this->registry = $this->createMock(TenantRegistry::class);
        $this->clock = $this->createMock(Clock::class);
        $this->ormConfiguration = $this->createOrmConfiguration();
    }

    #[Test]
    public function itReturnsEntityManagerForTenant(): void
    {
        $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
        $config = new TenantConfig(
            tenantId: $tenantId,
            subdomain: 'ecole-alpha',
            databaseUrl: 'sqlite:///:memory:',
        );

        $this->registry->method('getConfig')->with($tenantId)->willReturn($config);
        $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));

        $factory = new TenantEntityManagerFactory(
            $this->registry,
            $this->clock,
            $this->ormConfiguration,
        );

        $entityManager = $factory->getForTenant($tenantId);

        self::assertInstanceOf(EntityManagerInterface::class, $entityManager);
    }

    #[Test]
    public function itReturnsSameEntityManagerForSameTenant(): void
    {
        $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
        $config = new TenantConfig(
            tenantId: $tenantId,
            subdomain: 'ecole-alpha',
            databaseUrl: 'sqlite:///:memory:',
        );

        $this->registry->method('getConfig')->with($tenantId)->willReturn($config);
        $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));

        $factory = new TenantEntityManagerFactory(
            $this->registry,
            $this->clock,
            $this->ormConfiguration,
        );

        $em1 = $factory->getForTenant($tenantId);
        $em2 = $factory->getForTenant($tenantId);

        self::assertSame($em1, $em2);
    }

    #[Test]
    public function itReturnsDifferentEntityManagersForDifferentTenants(): void
    {
        $tenantIdAlpha = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
        $tenantIdBeta = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');

        $configAlpha = new TenantConfig(
            tenantId: $tenantIdAlpha,
            subdomain: 'ecole-alpha',
            databaseUrl: 'sqlite:///:memory:',
        );
        $configBeta = new TenantConfig(
            tenantId: $tenantIdBeta,
            subdomain: 'ecole-beta',
            databaseUrl: 'sqlite:///:memory:',
        );

        $this->registry->method('getConfig')->willReturnMap([
            [$tenantIdAlpha, $configAlpha],
            [$tenantIdBeta, $configBeta],
        ]);
        $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));

        $factory = new TenantEntityManagerFactory(
            $this->registry,
            $this->clock,
            $this->ormConfiguration,
        );

        $emAlpha = $factory->getForTenant($tenantIdAlpha);
        $emBeta = $factory->getForTenant($tenantIdBeta);

        self::assertNotSame($emAlpha, $emBeta);
    }

    #[Test]
    public function itReturnsCorrectPoolSize(): void
    {
        $tenantId1 = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
        $tenantId2 = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');

        $config1 = new TenantConfig(
            tenantId: $tenantId1,
            subdomain: 'ecole-alpha',
            databaseUrl: 'sqlite:///:memory:',
        );
        $config2 = new TenantConfig(
            tenantId: $tenantId2,
            subdomain: 'ecole-beta',
            databaseUrl: 'sqlite:///:memory:',
        );

        $this->registry->method('getConfig')->willReturnMap([
            [$tenantId1, $config1],
            [$tenantId2, $config2],
        ]);
        $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));

        $factory = new TenantEntityManagerFactory(
            $this->registry,
            $this->clock,
            $this->ormConfiguration,
        );

        self::assertSame(0, $factory->getPoolSize());

        $factory->getForTenant($tenantId1);
        self::assertSame(1, $factory->getPoolSize());

        $factory->getForTenant($tenantId2);
        self::assertSame(2, $factory->getPoolSize());

        // Accessing same tenant shouldn't increase pool size
        $factory->getForTenant($tenantId1);
        self::assertSame(2, $factory->getPoolSize());
    }

    #[Test]
    public function itEvictsIdleConnectionsAfterTimeout(): void
    {
        $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
        $config = new TenantConfig(
            tenantId: $tenantId,
            subdomain: 'ecole-alpha',
            databaseUrl: 'sqlite:///:memory:',
        );

        $this->registry->method('getConfig')->with($tenantId)->willReturn($config);

        $initialTime = new DateTimeImmutable('2026-01-30 10:00:00');
        $afterTimeout = new DateTimeImmutable('2026-01-30 10:06:00'); // 6 minutes later

        $this->clock->method('now')->willReturnOnConsecutiveCalls(
            $initialTime,    // First call - eviction check
            $initialTime,    // Store lastUsed
            $afterTimeout,   // Second call - eviction check (finds idle)
            $afterTimeout,   // Store lastUsed for new manager
        );

        $factory = new TenantEntityManagerFactory(
            $this->registry,
            $this->clock,
            $this->ormConfiguration,
        );

        $em1 = $factory->getForTenant($tenantId);
        $em2 = $factory->getForTenant($tenantId);

        // Due to idle eviction, we should have a new entity manager
        self::assertNotSame($em1, $em2);
    }

    private function createOrmConfiguration(): Configuration
    {
        $config = new Configuration();
        $config->setProxyDir(sys_get_temp_dir() . '/doctrine_proxies_' . uniqid());
        $config->setProxyNamespace('DoctrineProxies');
        $config->setAutoGenerateProxyClasses(true);
        $config->setMetadataDriverImpl(new AttributeDriver([]));
        $config->enableNativeLazyObjects(true);

        return $config;
    }
}
