<?php

declare(strict_types=1);

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

use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\StatutCompte;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\UserRepository;
use App\Administration\Infrastructure\Security\DatabaseUserProvider;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Administration\Infrastructure\Security\SecurityUserFactory;
use App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantResolver;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use stdClass;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;

final class DatabaseUserProviderTest extends TestCase
{
    private const string TENANT_ALPHA_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
    private const string TENANT_BETA_ID = 'b2c3d4e5-f6a7-8901-bcde-f12345678901';

    #[Test]
    public function loadUserByIdentifierReturnsSecurityUserForActiveAccount(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID);
        $domainUser = $this->createUser($tenantId, StatutCompte::ACTIF, hashedPassword: '$argon2id$hash');

        $repository = $this->createMock(UserRepository::class);
        $repository->method('findByEmail')
            ->with(
                self::callback(static fn (Email $email) => (string) $email === 'user@example.com'),
                self::callback(static fn (TenantId $id) => (string) $id === self::TENANT_ALPHA_ID)
            )
            ->willReturn($domainUser);

        $provider = $this->createProvider($repository, 'ecole-alpha.classeo.local');

        $securityUser = $provider->loadUserByIdentifier('user@example.com');

        self::assertInstanceOf(SecurityUser::class, $securityUser);
        self::assertSame((string) $domainUser->email, $securityUser->getUserIdentifier());
        self::assertSame((string) $domainUser->id, $securityUser->userId());
    }

    #[Test]
    public function loadUserByIdentifierThrowsForNonExistentUser(): void
    {
        $repository = $this->createMock(UserRepository::class);
        $repository->method('findByEmail')->willReturn(null);

        $provider = $this->createProvider($repository, 'ecole-alpha.classeo.local');

        $this->expectException(UserNotFoundException::class);

        $provider->loadUserByIdentifier('nonexistent@example.com');
    }

    #[Test]
    public function loadUserByIdentifierThrowsForInactiveAccount(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID);
        $domainUser = $this->createUser($tenantId, StatutCompte::EN_ATTENTE);

        $repository = $this->createMock(UserRepository::class);
        $repository->method('findByEmail')->willReturn($domainUser);

        $provider = $this->createProvider($repository, 'ecole-alpha.classeo.local');

        // Should throw because account is not active (AC2: no account existence revelation)
        $this->expectException(UserNotFoundException::class);

        $provider->loadUserByIdentifier('user@example.com');
    }

    #[Test]
    public function loadUserByIdentifierThrowsAccountStatusExceptionForSuspendedUser(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID);
        $domainUser = $this->createUser($tenantId, StatutCompte::SUSPENDU, hashedPassword: '$argon2id$hash');

        $repository = $this->createMock(UserRepository::class);
        $repository->method('findByEmail')->willReturn($domainUser);

        $provider = $this->createProvider($repository, 'ecole-alpha.classeo.local');

        $this->expectException(CustomUserMessageAccountStatusException::class);

        $provider->loadUserByIdentifier('user@example.com');
    }

    #[Test]
    public function loadUserByIdentifierThrowsForUnknownTenant(): void
    {
        $repository = $this->createMock(UserRepository::class);
        // Repository should not even be called if tenant is unknown
        $repository->expects(self::never())->method('findByEmail');

        $provider = $this->createProvider($repository, 'unknown-tenant.classeo.local');

        // Should throw generic error (don't reveal tenant doesn't exist)
        $this->expectException(UserNotFoundException::class);

        $provider->loadUserByIdentifier('user@example.com');
    }

    #[Test]
    public function loadUserByIdentifierUsesCorrectTenantFromRequest(): void
    {
        $tenantAlphaId = TenantId::fromString(self::TENANT_ALPHA_ID);
        $tenantBetaId = TenantId::fromString(self::TENANT_BETA_ID);

        // User exists in Alpha but not in Beta
        $domainUser = $this->createUser($tenantAlphaId, StatutCompte::ACTIF, hashedPassword: '$argon2id$hash');

        $repository = $this->createMock(UserRepository::class);
        $repository->method('findByEmail')
            ->willReturnCallback(static function (Email $email, TenantId $tenantId) use ($domainUser, $tenantAlphaId) {
                // Only return user if looking in Alpha tenant
                if ((string) $tenantId === (string) $tenantAlphaId) {
                    return $domainUser;
                }

                return null;
            });

        // Request comes from Beta tenant
        $provider = $this->createProvider($repository, 'ecole-beta.classeo.local');

        // Should throw because user doesn't exist in Beta tenant
        $this->expectException(UserNotFoundException::class);

        $provider->loadUserByIdentifier('user@example.com');
    }

    #[Test]
    public function refreshUserReloadsUserFromRepository(): void
    {
        $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID);
        $domainUser = $this->createUser($tenantId, StatutCompte::ACTIF, hashedPassword: '$argon2id$hash');

        $repository = $this->createMock(UserRepository::class);
        $repository->expects(self::once())
            ->method('findByEmail')
            ->willReturn($domainUser);

        $provider = $this->createProvider($repository, 'ecole-alpha.classeo.local');

        $factory = new SecurityUserFactory();
        $existingSecurityUser = $factory->fromDomainUser($domainUser);
        $refreshedUser = $provider->refreshUser($existingSecurityUser);

        self::assertInstanceOf(SecurityUser::class, $refreshedUser);
    }

    #[Test]
    public function supportsClassReturnsTrueForSecurityUser(): void
    {
        $repository = $this->createMock(UserRepository::class);
        $provider = $this->createProvider($repository, 'ecole-alpha.classeo.local');

        self::assertTrue($provider->supportsClass(SecurityUser::class));
        self::assertFalse($provider->supportsClass(stdClass::class));
    }

    #[Test]
    public function localhostFallsBackToEcoleAlphaTenant(): void
    {
        $tenantAlphaId = TenantId::fromString(self::TENANT_ALPHA_ID);
        $domainUser = $this->createUser($tenantAlphaId, StatutCompte::ACTIF, hashedPassword: '$argon2id$hash');

        $repository = $this->createMock(UserRepository::class);
        $repository->method('findByEmail')
            ->with(
                self::callback(static fn (Email $email) => (string) $email === 'user@example.com'),
                // Should use ecole-alpha tenant ID when accessed from localhost
                self::callback(static fn (TenantId $id) => (string) $id === self::TENANT_ALPHA_ID)
            )
            ->willReturn($domainUser);

        // Request from localhost should use ecole-alpha tenant
        $provider = $this->createProvider($repository, 'localhost');

        $securityUser = $provider->loadUserByIdentifier('user@example.com');

        self::assertInstanceOf(SecurityUser::class, $securityUser);
    }

    private function createProvider(UserRepository $repository, string $host): DatabaseUserProvider
    {
        $tenantRegistry = new InMemoryTenantRegistry([
            new TenantConfig(
                TenantId::fromString(self::TENANT_ALPHA_ID),
                'ecole-alpha',
                'postgresql://localhost/alpha'
            ),
            new TenantConfig(
                TenantId::fromString(self::TENANT_BETA_ID),
                'ecole-beta',
                'postgresql://localhost/beta'
            ),
        ]);

        $tenantResolver = new TenantResolver($tenantRegistry, 'classeo.local');

        $request = Request::create('https://' . $host . '/api/login');
        $requestStack = new RequestStack();
        $requestStack->push($request);

        return new DatabaseUserProvider($repository, $tenantResolver, $requestStack, new SecurityUserFactory());
    }

    private function createUser(TenantId $tenantId, StatutCompte $statut, ?string $hashedPassword = null): User
    {
        return User::reconstitute(
            id: UserId::generate(),
            email: new Email('user@example.com'),
            roles: [Role::PARENT],
            tenantId: $tenantId,
            schoolName: 'École Test',
            statut: $statut,
            dateNaissance: null,
            createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
            hashedPassword: $hashedPassword,
            activatedAt: $statut === StatutCompte::ACTIF ? new DateTimeImmutable() : null,
            consentementParental: null,
        );
    }
}
