<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Exception\UserNotFoundException;
use App\Administration\Domain\Model\ConsentementParental\ConsentementParental;
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\Infrastructure\Persistence\Doctrine\DoctrineUserRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class DoctrineUserRepositoryTest 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 saveExecutesUpsertStatement(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->expects(self::once())
            ->method('executeStatement')
            ->with(
                self::stringContains('INSERT INTO users'),
                self::callback(static function (array $params): bool {
                    return $params['email'] === 'test@example.com'
                        && $params['statut'] === 'pending'
                        && $params['school_name'] === 'École Test'
                        && str_contains($params['roles'], 'ROLE_PARENT');
                }),
            );

        $repository = new DoctrineUserRepository($connection);

        $user = User::creer(
            email: new Email('test@example.com'),
            role: Role::PARENT,
            tenantId: TenantId::fromString(self::TENANT_ALPHA_ID),
            schoolName: 'École Test',
            dateNaissance: null,
            createdAt: new DateTimeImmutable('2026-01-15T10:00:00+00:00'),
        );

        $repository->save($user);
    }

    #[Test]
    public function findByIdReturnsUserWhenFound(): void
    {
        $userId = '550e8400-e29b-41d4-a716-446655440001';

        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')
            ->willReturn($this->makeRow($userId));

        $repository = new DoctrineUserRepository($connection);

        $user = $repository->findById(UserId::fromString($userId));

        self::assertNotNull($user);
        self::assertSame($userId, (string) $user->id);
        self::assertSame('test@example.com', (string) $user->email);
        self::assertSame(Role::PARENT, $user->role);
        self::assertSame('École Test', $user->schoolName);
    }

    #[Test]
    public function findByIdReturnsNullWhenNotFound(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')->willReturn(false);

        $repository = new DoctrineUserRepository($connection);

        $user = $repository->findById(UserId::fromString('550e8400-e29b-41d4-a716-446655440001'));

        self::assertNull($user);
    }

    #[Test]
    public function getThrowsWhenUserNotFound(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')->willReturn(false);

        $repository = new DoctrineUserRepository($connection);

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

        $repository->get(UserId::fromString('550e8400-e29b-41d4-a716-446655440001'));
    }

    #[Test]
    public function findByEmailReturnsUserWhenFound(): void
    {
        $userId = '550e8400-e29b-41d4-a716-446655440001';
        $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID);

        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')
            ->with(
                self::stringContains('tenant_id = :tenant_id AND email = :email'),
                self::callback(static fn (array $params) => $params['email'] === 'test@example.com'
                    && $params['tenant_id'] === self::TENANT_ALPHA_ID),
            )
            ->willReturn($this->makeRow($userId));

        $repository = new DoctrineUserRepository($connection);

        $user = $repository->findByEmail(new Email('test@example.com'), $tenantId);

        self::assertNotNull($user);
        self::assertSame($userId, (string) $user->id);
    }

    #[Test]
    public function findByEmailReturnsNullForDifferentTenant(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')->willReturn(false);

        $repository = new DoctrineUserRepository($connection);

        $user = $repository->findByEmail(
            new Email('test@example.com'),
            TenantId::fromString(self::TENANT_BETA_ID),
        );

        self::assertNull($user);
    }

    #[Test]
    public function findAllByTenantReturnsUsersForTenant(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAllAssociative')
            ->willReturn([
                $this->makeRow('550e8400-e29b-41d4-a716-446655440001'),
                $this->makeRow('550e8400-e29b-41d4-a716-446655440002', 'other@example.com'),
            ]);

        $repository = new DoctrineUserRepository($connection);

        $users = $repository->findAllByTenant(TenantId::fromString(self::TENANT_ALPHA_ID));

        self::assertCount(2, $users);
    }

    #[Test]
    public function hydrateHandlesConsentementParental(): void
    {
        $userId = '550e8400-e29b-41d4-a716-446655440001';
        $row = $this->makeRow($userId);
        $row['consentement_parent_id'] = '660e8400-e29b-41d4-a716-446655440001';
        $row['consentement_eleve_id'] = '770e8400-e29b-41d4-a716-446655440001';
        $row['consentement_date'] = '2026-01-20T14:00:00+00:00';
        $row['consentement_ip'] = '192.168.1.1';

        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')->willReturn($row);

        $repository = new DoctrineUserRepository($connection);

        $user = $repository->findById(UserId::fromString($userId));

        self::assertNotNull($user);
        self::assertNotNull($user->consentementParental);
        self::assertSame('660e8400-e29b-41d4-a716-446655440001', $user->consentementParental->parentId);
        self::assertSame('770e8400-e29b-41d4-a716-446655440001', $user->consentementParental->eleveId);
        self::assertSame('192.168.1.1', $user->consentementParental->ipAddress);
    }

    #[Test]
    public function hydrateHandlesMultipleRoles(): void
    {
        $userId = '550e8400-e29b-41d4-a716-446655440001';
        $row = $this->makeRow($userId);
        $row['roles'] = '["ROLE_PROF", "ROLE_ADMIN"]';

        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')->willReturn($row);

        $repository = new DoctrineUserRepository($connection);

        $user = $repository->findById(UserId::fromString($userId));

        self::assertNotNull($user);
        self::assertCount(2, $user->roles);
        self::assertSame(Role::PROF, $user->roles[0]);
        self::assertSame(Role::ADMIN, $user->roles[1]);
    }

    #[Test]
    public function hydrateHandlesBlockedUser(): void
    {
        $userId = '550e8400-e29b-41d4-a716-446655440001';
        $row = $this->makeRow($userId);
        $row['statut'] = StatutCompte::SUSPENDU->value;
        $row['blocked_at'] = '2026-01-20T14:00:00+00:00';
        $row['blocked_reason'] = 'Comportement inapproprié';

        $connection = $this->createMock(Connection::class);
        $connection->method('fetchAssociative')->willReturn($row);

        $repository = new DoctrineUserRepository($connection);

        $user = $repository->findById(UserId::fromString($userId));

        self::assertNotNull($user);
        self::assertSame(StatutCompte::SUSPENDU, $user->statut);
        self::assertNotNull($user->blockedAt);
        self::assertSame('Comportement inapproprié', $user->blockedReason);
    }

    #[Test]
    public function savePreservesConsentementParental(): void
    {
        $consentement = ConsentementParental::accorder(
            parentId: '660e8400-e29b-41d4-a716-446655440001',
            eleveId: '770e8400-e29b-41d4-a716-446655440001',
            at: new DateTimeImmutable('2026-01-20T14:00:00+00:00'),
            ipAddress: '192.168.1.1',
        );

        $connection = $this->createMock(Connection::class);
        $connection->expects(self::once())
            ->method('executeStatement')
            ->with(
                self::anything(),
                self::callback(static function (array $params) {
                    return $params['consentement_parent_id'] === '660e8400-e29b-41d4-a716-446655440001'
                        && $params['consentement_eleve_id'] === '770e8400-e29b-41d4-a716-446655440001'
                        && $params['consentement_ip'] === '192.168.1.1';
                }),
            );

        $repository = new DoctrineUserRepository($connection);

        $user = User::reconstitute(
            id: UserId::fromString('550e8400-e29b-41d4-a716-446655440001'),
            email: new Email('minor@example.com'),
            roles: [Role::ELEVE],
            tenantId: TenantId::fromString(self::TENANT_ALPHA_ID),
            schoolName: 'École Test',
            statut: StatutCompte::EN_ATTENTE,
            dateNaissance: new DateTimeImmutable('2015-05-15'),
            createdAt: new DateTimeImmutable('2026-01-15T10:00:00+00:00'),
            hashedPassword: null,
            activatedAt: null,
            consentementParental: $consentement,
        );

        $repository->save($user);
    }

    /**
     * @return array<string, mixed>
     */
    private function makeRow(string $userId, string $email = 'test@example.com'): array
    {
        return [
            'id' => $userId,
            'tenant_id' => self::TENANT_ALPHA_ID,
            'email' => $email,
            'first_name' => 'Jean',
            'last_name' => 'Dupont',
            'roles' => '["ROLE_PARENT"]',
            'hashed_password' => null,
            'statut' => 'pending',
            'school_name' => 'École Test',
            'date_naissance' => null,
            'created_at' => '2026-01-15T10:00:00+00:00',
            'activated_at' => null,
            'invited_at' => null,
            'blocked_at' => null,
            'blocked_reason' => null,
            'consentement_parent_id' => null,
            'consentement_eleve_id' => null,
            'consentement_date' => null,
            'consentement_ip' => null,
        ];
    }
}
