<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Tests\Components;

use Ferienpass\CmsBundle\Application\FriendCodeManager;
use Ferienpass\CmsBundle\Components\ApplicationForm;
use Ferienpass\CoreBundle\Application\ApplicationFormBuilder;
use Ferienpass\CoreBundle\Application\ExtraFileManager;
use Ferienpass\CoreBundle\ApplicationSystem\ApplicationSystems;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentRequirementChecker;
use Ferienpass\CoreBundle\Entity\AttendanceExtraFile;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Facade\PaymentsFacade;
use Ferienpass\CoreBundle\Repository\AttendanceExtraFileRepository;
use Ferienpass\CoreBundle\Repository\AttendanceRepository;
use Ferienpass\CoreBundle\Repository\EditionTaskRepository;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use Ferienpass\CoreBundle\Settings\SystemSettingsManager;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * The submit gate for extra questions: only required ones block, and a `file`
 * question counts as answered only when a real upload is staged — never because
 * the (client-writable) scalar answer happens to hold a filename.
 */
final class ApplicationFormExtraFilesTest extends TestCase
{
    private const TEXT = '0192d1f0-0000-7000-8000-00000000000a';
    private const FILE = '0192d1f0-0000-7000-8000-00000000000b';

    public function testOptionalQuestionsDoNotBlockTheSubmit(): void
    {
        $form = $this->form([
            ['uuid' => self::TEXT, 'question' => 'Allergien?', 'type' => 'txt', 'required' => false],
            ['uuid' => self::FILE, 'question' => 'Nachweis?', 'type' => 'file', 'required' => false, 'fileTypes' => ['pdf']],
        ]);

        self::assertSame([], $form->missingRequiredExtras());
    }

    public function testRequiredTextQuestionBlocksUntilAnswered(): void
    {
        $form = $this->form([
            ['uuid' => self::TEXT, 'question' => 'Allergien?', 'type' => 'txt', 'required' => true],
        ]);

        self::assertSame([self::TEXT], $form->missingRequiredExtras());

        // Whitespace is not an answer.
        $form->extra[self::TEXT] = '   ';
        self::assertSame([self::TEXT], $form->missingRequiredExtras());

        $form->extra[self::TEXT] = 'Nussallergie';
        self::assertSame([], $form->missingRequiredExtras());
    }

    public function testRequiredFileQuestionIsNotSatisfiedByAScalarAnswerAlone(): void
    {
        $form = $this->form([
            ['uuid' => self::FILE, 'question' => 'Nachweis?', 'type' => 'file', 'required' => true, 'fileTypes' => ['pdf']],
        ]);

        // What a tampered client — or the leftover state of a previous submit —
        // would look like: a filename, no file.
        $form->extra[self::FILE] = 'nachweis.pdf';

        self::assertSame([self::FILE], $form->missingRequiredExtras());
    }

    public function testRequiredFileQuestionIsSatisfiedByAStagedUpload(): void
    {
        $staged = new AttendanceExtraFile(self::FILE, 'application/a.pdf', 'nachweis.pdf', 'application/pdf', 12);

        $files = $this->createMock(AttendanceExtraFileRepository::class);
        $files->method('find')->willReturn($staged);

        $form = $this->form([
            ['uuid' => self::FILE, 'question' => 'Nachweis?', 'type' => 'file', 'required' => true, 'fileTypes' => ['pdf']],
        ], $files);

        $form->extraFiles = [self::FILE => $staged->getUuid()->toRfc4122()];

        self::assertSame([], $form->missingRequiredExtras());
        self::assertSame($staged, $form->extraFile(self::FILE));
    }

    public function testStagedFileOfAnotherQuestionIsNotAccepted(): void
    {
        // A row that exists but answers a different question must not be read back
        // as this question's answer.
        $other = new AttendanceExtraFile(self::TEXT, 'application/a.pdf', 'nachweis.pdf', 'application/pdf', 12);

        $files = $this->createMock(AttendanceExtraFileRepository::class);
        $files->method('find')->willReturn($other);

        $form = $this->form([
            ['uuid' => self::FILE, 'question' => 'Nachweis?', 'type' => 'file', 'required' => true, 'fileTypes' => ['pdf']],
        ], $files);

        $form->extraFiles = [self::FILE => $other->getUuid()->toRfc4122()];

        self::assertNull($form->extraFile(self::FILE));
        self::assertSame([self::FILE], $form->missingRequiredExtras());
    }

    public function testStalePropRendersAsNoFileInsteadOfFailing(): void
    {
        // The prune cron removed the staged row while the form sat open.
        $files = $this->createMock(AttendanceExtraFileRepository::class);
        $files->method('find')->willReturn(null);

        $form = $this->form([
            ['uuid' => self::FILE, 'question' => 'Nachweis?', 'type' => 'file', 'required' => false, 'fileTypes' => ['pdf']],
        ], $files);

        $form->extraFiles = [self::FILE => Uuid::v7()->toRfc4122()];

        self::assertNull($form->extraFile(self::FILE));
    }

    public function testUnknownQuestionIdIsRejected(): void
    {
        $form = $this->form([
            ['uuid' => self::TEXT, 'question' => 'Allergien?', 'type' => 'txt', 'required' => false],
        ]);

        self::assertNotNull($form->extraQuestion(self::TEXT));
        self::assertNull($form->extraQuestion(self::FILE));
    }

    private function form(array $extras, ?AttendanceExtraFileRepository $files = null): ApplicationForm
    {
        $offer = new Base();
        $offer->setApplicationExtra($extras);

        $form = new ApplicationForm(
            $this->createStub(ApplicationSystems::class),
            $this->createStub(AttendanceRepository::class),
            $this->createStub(ParticipantRepositoryInterface::class),
            $this->createStub(RequestStack::class),
            $this->createStub(EventDispatcherInterface::class),
            $this->createStub(SystemSettingsManager::class),
            $this->createStub(PaymentsFacade::class),
            $this->createStub(EditionTaskRepository::class),
            $this->createStub(ConsentManager::class),
            $this->createStub(ConsentRequirementChecker::class),
            $this->createStub(ApplicationFormBuilder::class),
            $this->createStub(FriendCodeManager::class),
            new RateLimiterFactory(['id' => 'test', 'policy' => 'no_limit'], new InMemoryStorage()),
            new RateLimiterFactory(['id' => 'test-files', 'policy' => 'no_limit'], new InMemoryStorage()),
            $this->createStub(ExtraFileManager::class),
            $files ?? $this->createStub(AttendanceExtraFileRepository::class),
        );

        $form->offer = $offer;

        return $form;
    }
}
