<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Api\Processor;

use ApiPlatform\Metadata\Put;
use App\Administration\Application\Command\UpdateUserRoles\UpdateUserRolesHandler;
use App\Administration\Application\Port\ActiveRoleStore;
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\Api\Processor\UpdateUserRolesProcessor;
use App\Administration\Infrastructure\Api\Resource\UserResource;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Administration\Infrastructure\Security\UserVoter;
use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

/**
 * Tests for UpdateUserRolesProcessor.
 *
 * Key invariants:
 * - Only ADMIN/SUPER_ADMIN can manage roles
 * - Non-SUPER_ADMIN cannot assign SUPER_ADMIN
 * - Admin cannot self-demote
 * - Domain events are dispatched after successful update
 * - Tenant context must be set
 */
final class UpdateUserRolesProcessorTest extends TestCase
{
    private const string USER_ID = '550e8400-e29b-41d4-a716-446655440001';
    private const string ADMIN_USER_ID = '550e8400-e29b-41d4-a716-446655440099';
    private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';

    private InMemoryUserRepository $userRepository;
    private MessageBusInterface $eventBus;
    private AuthorizationCheckerInterface $authChecker;
    private TenantContext $tenantContext;
    private Clock $clock;
    private Security $security;
    private ActiveRoleStore $activeRoleStore;

    protected function setUp(): void
    {
        $this->userRepository = new InMemoryUserRepository();
        $this->eventBus = $this->createMock(MessageBusInterface::class);
        $this->authChecker = $this->createMock(AuthorizationCheckerInterface::class);
        $this->tenantContext = new TenantContext();
        $this->clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-15 10:00:00');
            }
        };
        $this->security = $this->createMock(Security::class);
        $this->activeRoleStore = new class implements ActiveRoleStore {
            public function store(User $user, Role $role): void
            {
            }

            public function get(User $user): ?Role
            {
                return null;
            }

            public function clear(User $user): void
            {
            }
        };
    }

    #[Test]
    public function updatesRolesSuccessfully(): void
    {
        $this->authorizeManageRoles();
        $this->setTenant();
        $this->authenticateAsAdmin();
        $this->saveTargetUser([Role::PROF]);

        $this->eventBus->method('dispatch')
            ->willReturnCallback(static fn (object $msg) => new Envelope($msg));

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = [Role::PROF->value, Role::ADMIN->value];

        $output = $processor->process($data, new Put(), ['id' => self::USER_ID]);

        self::assertInstanceOf(UserResource::class, $output);
        self::assertSame(self::USER_ID, $output->id);
        self::assertContains(Role::ADMIN->value, $output->roles);
        self::assertContains(Role::PROF->value, $output->roles);
    }

    #[Test]
    public function throwsAccessDeniedWhenNotAuthorized(): void
    {
        $this->authChecker->method('isGranted')
            ->with(UserVoter::MANAGE_ROLES)
            ->willReturn(false);

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = [Role::PROF->value];

        $this->expectException(AccessDeniedHttpException::class);
        $this->expectExceptionMessage('autoris');

        $processor->process($data, new Put(), ['id' => self::USER_ID]);
    }

    #[Test]
    public function throwsUnauthorizedWhenNoTenant(): void
    {
        $this->authorizeManageRoles();
        // tenantContext NOT set

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = [Role::PROF->value];

        $this->expectException(UnauthorizedHttpException::class);
        $this->expectExceptionMessage('Tenant');

        $processor->process($data, new Put(), ['id' => self::USER_ID]);
    }

    #[Test]
    public function preventsPrivilegeEscalationToSuperAdmin(): void
    {
        $this->authorizeManageRoles();
        $this->setTenant();
        $this->authenticateAsAdmin();

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = [Role::SUPER_ADMIN->value];

        $this->expectException(AccessDeniedHttpException::class);
        $this->expectExceptionMessage('super administrateur');

        $processor->process($data, new Put(), ['id' => self::USER_ID]);
    }

    #[Test]
    public function allowsSuperAdminToAssignSuperAdmin(): void
    {
        $this->authorizeManageRoles();
        $this->setTenant();
        $this->authenticateAsSuperAdmin();
        $this->saveTargetUser([Role::PROF]);

        $this->eventBus->method('dispatch')
            ->willReturnCallback(static fn (object $msg) => new Envelope($msg));

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = [Role::SUPER_ADMIN->value];

        $output = $processor->process($data, new Put(), ['id' => self::USER_ID]);

        self::assertInstanceOf(UserResource::class, $output);
        self::assertContains(Role::SUPER_ADMIN->value, $output->roles);
    }

    #[Test]
    public function preventsAdminSelfDemotion(): void
    {
        $this->authorizeManageRoles();
        $this->setTenant();
        $this->authenticateAsAdmin();

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = [Role::PROF->value];

        $this->expectException(BadRequestHttpException::class);
        $this->expectExceptionMessage('propre r');

        $processor->process($data, new Put(), ['id' => self::ADMIN_USER_ID]);
    }

    #[Test]
    public function throwsNotFoundWhenUserDoesNotExist(): void
    {
        $this->authorizeManageRoles();
        $this->setTenant();
        $this->authenticateAsAdmin();
        // Target user NOT saved

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = [Role::PROF->value];

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

        $processor->process($data, new Put(), ['id' => self::USER_ID]);
    }

    #[Test]
    public function throwsBadRequestOnInvalidRole(): void
    {
        $this->authorizeManageRoles();
        $this->setTenant();
        $this->authenticateAsAdmin();
        $this->saveTargetUser([Role::PROF]);

        $processor = $this->createProcessor();

        $data = new UserResource();
        $data->roles = ['ROLE_INVALID'];

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

        $processor->process($data, new Put(), ['id' => self::USER_ID]);
    }

    private function createProcessor(): UpdateUserRolesProcessor
    {
        $handler = new UpdateUserRolesHandler(
            $this->userRepository,
            $this->clock,
            $this->activeRoleStore,
        );

        return new UpdateUserRolesProcessor(
            $handler,
            $this->eventBus,
            $this->authChecker,
            $this->tenantContext,
            $this->clock,
            $this->security,
        );
    }

    private function authorizeManageRoles(): void
    {
        $this->authChecker->method('isGranted')
            ->with(UserVoter::MANAGE_ROLES)
            ->willReturn(true);
    }

    private function setTenant(): void
    {
        $this->tenantContext->setCurrentTenant(
            new TenantConfig(
                TenantId::fromString(self::TENANT_ID),
                'ecole-alpha',
                'sqlite:///:memory:',
            ),
        );
    }

    private function authenticateAsAdmin(): void
    {
        $securityUser = new SecurityUser(
            userId: UserId::fromString(self::ADMIN_USER_ID),
            email: 'admin@example.com',
            hashedPassword: '$argon2id$hashed',
            tenantId: TenantId::fromString(self::TENANT_ID),
            roles: [Role::ADMIN->value],
        );
        $this->security->method('getUser')->willReturn($securityUser);
    }

    private function authenticateAsSuperAdmin(): void
    {
        $securityUser = new SecurityUser(
            userId: UserId::fromString(self::ADMIN_USER_ID),
            email: 'superadmin@example.com',
            hashedPassword: '$argon2id$hashed',
            tenantId: TenantId::fromString(self::TENANT_ID),
            roles: [Role::SUPER_ADMIN->value, Role::ADMIN->value],
        );
        $this->security->method('getUser')->willReturn($securityUser);
    }

    private function saveTargetUser(array $roles): void
    {
        $user = User::reconstitute(
            id: UserId::fromString(self::USER_ID),
            email: new Email('target@example.com'),
            roles: $roles,
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: 'Ecole Test',
            statut: StatutCompte::ACTIF,
            dateNaissance: null,
            createdAt: new DateTimeImmutable('2026-01-15T10:00:00+00:00'),
            hashedPassword: '$argon2id$hashed',
            activatedAt: new DateTimeImmutable('2026-01-16T10:00:00+00:00'),
            consentementParental: null,
            firstName: 'Jean',
            lastName: 'Dupont',
        );
        $this->userRepository->save($user);
    }
}
