<?php

declare(strict_types=1);

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

use ApiPlatform\Metadata\Post;
use App\Administration\Application\Command\LinkParentToStudent\LinkParentToStudentHandler;
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\UserRepository;
use App\Administration\Infrastructure\Api\Processor\LinkParentToStudentProcessor;
use App\Administration\Infrastructure\Api\Resource\StudentGuardianResource;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Administration\Infrastructure\Security\StudentGuardianVoter;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId as InfraTenantId;
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\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class LinkParentToStudentProcessorTest extends TestCase
{
    private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
    private const string SUBDOMAIN = 'ecole-alpha';
    private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440010';
    private const string GUARDIAN_ID = '550e8400-e29b-41d4-a716-446655440020';
    private const string GUARDIAN_ID_2 = '550e8400-e29b-41d4-a716-446655440021';
    private const string GUARDIAN_ID_3 = '550e8400-e29b-41d4-a716-446655440022';

    private InMemoryStudentGuardianRepository $repository;
    private TenantContext $tenantContext;
    private Clock $clock;
    private SecurityUser $securityUser;

    protected function setUp(): void
    {
        $this->repository = new InMemoryStudentGuardianRepository();
        $this->clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-10 10:00:00');
            }
        };

        $this->tenantContext = new TenantContext();
        $this->tenantContext->setCurrentTenant(new TenantConfig(
            tenantId: InfraTenantId::fromString(self::TENANT_ID),
            subdomain: self::SUBDOMAIN,
            databaseUrl: 'postgresql://test',
        ));

        $this->securityUser = new SecurityUser(
            userId: UserId::fromString(self::GUARDIAN_ID),
            email: 'admin@example.com',
            hashedPassword: '$argon2id$hashed',
            tenantId: TenantId::fromString(self::TENANT_ID),
            roles: [Role::ADMIN->value],
        );
    }

    #[Test]
    public function linksParentToStudentSuccessfully(): void
    {
        $processor = $this->createProcessor();

        $data = new StudentGuardianResource();
        $data->guardianId = self::GUARDIAN_ID;
        $data->relationshipType = 'père';

        $result = $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);

        self::assertInstanceOf(StudentGuardianResource::class, $result);
        self::assertSame(self::STUDENT_ID, $result->studentId);
        self::assertSame(self::GUARDIAN_ID, $result->guardianId);
        self::assertSame('père', $result->relationshipType);
        self::assertSame('Père', $result->relationshipLabel);
        self::assertNotNull($result->id);
        self::assertNotNull($result->linkedAt);
    }

    #[Test]
    public function dispatchesDomainEventsAfterLinking(): void
    {
        $dispatched = [];
        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->method('dispatch')->willReturnCallback(
            static function (object $message) use (&$dispatched): Envelope {
                $dispatched[] = $message;

                return new Envelope($message);
            },
        );

        $processor = $this->createProcessor(eventBus: $eventBus);

        $data = new StudentGuardianResource();
        $data->guardianId = self::GUARDIAN_ID;
        $data->relationshipType = 'père';

        $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);

        self::assertNotEmpty($dispatched, 'At least one domain event should be dispatched.');
    }

    #[Test]
    public function throwsAccessDeniedWhenNotAuthorized(): void
    {
        $authChecker = $this->createMock(AuthorizationCheckerInterface::class);
        $authChecker->method('isGranted')
            ->with(StudentGuardianVoter::MANAGE)
            ->willReturn(false);

        $processor = $this->createProcessor(authorizationChecker: $authChecker);

        $data = new StudentGuardianResource();
        $data->guardianId = self::GUARDIAN_ID;
        $data->relationshipType = 'père';

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

        $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);
    }

    #[Test]
    public function throwsUnauthorizedWhenNoTenant(): void
    {
        $tenantContext = new TenantContext();

        $processor = $this->createProcessor(tenantContext: $tenantContext);

        $data = new StudentGuardianResource();
        $data->guardianId = self::GUARDIAN_ID;
        $data->relationshipType = 'père';

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

        $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);
    }

    #[Test]
    public function throwsBadRequestOnInvalidArgument(): void
    {
        $processor = $this->createProcessor();

        $data = new StudentGuardianResource();
        $data->guardianId = 'not-a-valid-uuid';
        $data->relationshipType = 'père';

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

        $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);
    }

    #[Test]
    public function throwsConflictWhenLinkAlreadyExists(): void
    {
        $existingLink = StudentGuardian::lier(
            studentId: UserId::fromString(self::STUDENT_ID),
            guardianId: UserId::fromString(self::GUARDIAN_ID),
            relationshipType: RelationshipType::FATHER,
            tenantId: TenantId::fromString(self::TENANT_ID),
            createdAt: new DateTimeImmutable('2026-02-09 10:00:00'),
        );
        $this->repository->save($existingLink);

        $processor = $this->createProcessor();

        $data = new StudentGuardianResource();
        $data->guardianId = self::GUARDIAN_ID;
        $data->relationshipType = 'père';

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

        $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);
    }

    #[Test]
    public function throwsUnprocessableWhenMaxGuardiansReached(): void
    {
        $link1 = StudentGuardian::lier(
            studentId: UserId::fromString(self::STUDENT_ID),
            guardianId: UserId::fromString(self::GUARDIAN_ID),
            relationshipType: RelationshipType::FATHER,
            tenantId: TenantId::fromString(self::TENANT_ID),
            createdAt: new DateTimeImmutable('2026-02-09 10:00:00'),
        );
        $this->repository->save($link1);

        $link2 = StudentGuardian::lier(
            studentId: UserId::fromString(self::STUDENT_ID),
            guardianId: UserId::fromString(self::GUARDIAN_ID_2),
            relationshipType: RelationshipType::MOTHER,
            tenantId: TenantId::fromString(self::TENANT_ID),
            createdAt: new DateTimeImmutable('2026-02-09 10:00:00'),
        );
        $this->repository->save($link2);

        $processor = $this->createProcessor();

        $data = new StudentGuardianResource();
        $data->guardianId = self::GUARDIAN_ID_3;
        $data->relationshipType = 'tuteur';

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

        $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);
    }

    #[Test]
    public function passesCurrentUserAsCreatedBy(): void
    {
        $expectedUserId = '550e8400-e29b-41d4-a716-446655440099';
        $securityUser = new SecurityUser(
            userId: UserId::fromString($expectedUserId),
            email: 'admin@example.com',
            hashedPassword: '$argon2id$hashed',
            tenantId: TenantId::fromString(self::TENANT_ID),
            roles: [Role::ADMIN->value],
        );

        $security = $this->createMock(Security::class);
        $security->method('getUser')->willReturn($securityUser);

        $processor = $this->createProcessor(security: $security);

        $data = new StudentGuardianResource();
        $data->guardianId = self::GUARDIAN_ID;
        $data->relationshipType = 'père';

        $result = $processor->process($data, new Post(), ['studentId' => self::STUDENT_ID]);

        self::assertInstanceOf(StudentGuardianResource::class, $result);
        self::assertSame(self::STUDENT_ID, $result->studentId);
    }

    private function createProcessor(
        ?TenantContext $tenantContext = null,
        ?AuthorizationCheckerInterface $authorizationChecker = null,
        ?MessageBusInterface $eventBus = null,
        ?Security $security = null,
    ): LinkParentToStudentProcessor {
        $now = new DateTimeImmutable('2026-02-10 10:00:00');
        $domainTenantId = TenantId::fromString(self::TENANT_ID);

        $guardianUser = User::creer(
            email: new Email('guardian@example.com'),
            role: Role::PARENT,
            tenantId: $domainTenantId,
            schoolName: 'École Test',
            dateNaissance: null,
            createdAt: $now,
        );

        $studentUser = User::creer(
            email: new Email('student@example.com'),
            role: Role::ELEVE,
            tenantId: $domainTenantId,
            schoolName: 'École Test',
            dateNaissance: null,
            createdAt: $now,
        );

        $userRepository = $this->createMock(UserRepository::class);
        $userRepository->method('get')->willReturnCallback(
            static function (UserId $id) use ($guardianUser, $studentUser): User {
                if ((string) $id === self::GUARDIAN_ID || (string) $id === self::GUARDIAN_ID_2 || (string) $id === self::GUARDIAN_ID_3) {
                    return $guardianUser;
                }

                return $studentUser;
            },
        );

        $handler = new LinkParentToStudentHandler($this->repository, $userRepository, $this->clock);

        $tenantContext ??= $this->tenantContext;

        if ($authorizationChecker === null) {
            $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class);
            $authorizationChecker->method('isGranted')
                ->with(StudentGuardianVoter::MANAGE)
                ->willReturn(true);
        }

        if ($eventBus === null) {
            $eventBus = $this->createMock(MessageBusInterface::class);
            $eventBus->method('dispatch')->willReturnCallback(
                static fn (object $message) => new Envelope($message),
            );
        }

        if ($security === null) {
            $security = $this->createMock(Security::class);
            $security->method('getUser')->willReturn($this->securityUser);
        }

        return new LinkParentToStudentProcessor(
            $handler,
            $tenantContext,
            $authorizationChecker,
            $eventBus,
            $security,
        );
    }
}
