<?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 Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CmsBundle\Application\FriendCodeManager;
use Ferienpass\CmsBundle\Components\ApplicationForm;
use Ferienpass\CoreBundle\Application\ApplicationFormBuilder;
use Ferienpass\CoreBundle\Application\ExtraFileManager;
use Ferienpass\CoreBundle\ApplicationSystem\ApplicationSystemInterface;
use Ferienpass\CoreBundle\ApplicationSystem\ApplicationSystems;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentRequirementChecker;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\EditionTask;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Facade\AttendanceFacade;
use Ferienpass\CoreBundle\Facade\PaymentsFacade;
use Ferienpass\CoreBundle\Message\AnonymousAttendanceUnconfirmed;
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\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Scenario 3: anonymous applicant submits a complete one-step apply on a free,
 * anonymous-enabled offer.
 *
 * What the test covers:
 *  - The participant is persisted (user remains null).
 *  - `createAwaitingEmailConfirmation` is called → held attendance.
 *  - The `AnonymousAttendanceUnconfirmed` message is dispatched.
 *  - A required-but-incomplete postal address rejects the submit cleanly.
 *  - A missing phone/mobile is rejected before any persistence happens.
 */
final class ApplicationFormAnonymousApplyTest extends TestCase
{
    public function testHappyPathPersistsParticipantHoldsAttendanceAndDispatchesConfirmation(): void
    {
        $offer = $this->offer();
        $participant = $this->fullyValidNewParticipant();

        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->expects(self::atLeastOnce())->method('persist')->with($participant);
        $entityManager->expects(self::atLeastOnce())->method('flush');

        $heldAttendance = new Attendance($offer, $participant);
        $heldAttendance->setStatus(AttendanceStatus::EMAIL_UNCONFIRMED);

        $facade = $this->createMock(AttendanceFacade::class);
        $facade->expects(self::once())
            ->method('createAwaitingEmailConfirmation')
            ->with($offer, $participant, true, false)
            ->willReturn($heldAttendance);

        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->expects(self::once())
            ->method('dispatch')
            ->with(self::callback(static fn (object $msg) => $msg instanceof AnonymousAttendanceUnconfirmed))
            ->willReturn(new Envelope(new \stdClass()));

        $component = $this->component(
            requirePostalAddress: false,
            allowAnonymous: true,
            consentMgr: $this->stubConsentManager(),
        );
        $component->offer = $offer;
        $component->newParticipant = $participant;

        $component->applyAnonymous(
            request: Request::create('/'),
            entityManager: $entityManager,
            attendanceFacade: $facade,
            eventBus: $eventBus,
            translator: $this->stubTranslator(),
        );

        self::assertNull($component->anonymousError, 'No error should surface on the happy path.');
        self::assertNotNull($component->confirm, 'A confirmation TranslatableMessage should be set.');
        // The browser-local participant id store should now include this participant's UUID.
        self::assertContains((string) $participant->getUuid(), $component->participantIds);
    }

    public function testRejectsWhenAnonymousNotAllowed(): void
    {
        $component = $this->component(allowAnonymous: false);
        $component->offer = $this->offer();
        $component->newParticipant = $this->fullyValidNewParticipant();

        // No facade interaction expected — rejected at the guard.
        $facade = $this->createMock(AttendanceFacade::class);
        $facade->expects(self::never())->method('createAwaitingEmailConfirmation');

        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->expects(self::never())->method('dispatch');

        $component->applyAnonymous(
            request: Request::create('/'),
            entityManager: $this->createMock(EntityManagerInterface::class),
            attendanceFacade: $facade,
            eventBus: $eventBus,
            translator: $this->stubTranslator(),
        );

        // The guard returns silently; nothing should have been written.
        self::assertNull($component->confirm);
    }

    public function testRejectsWhenPhoneAndMobileBothMissing(): void
    {
        $participant = $this->makeParticipant(
            firstname: 'Anna',
            lastname: 'M.',
            email: 'anna@example.com',
            dob: '2015-06-14',
            phone: null,
            mobile: null,
        );

        $component = $this->component(allowAnonymous: true);
        $component->offer = $this->offer();
        $component->newParticipant = $participant;

        $facade = $this->createMock(AttendanceFacade::class);
        $facade->expects(self::never())->method('createAwaitingEmailConfirmation');
        $eventBus = $this->createMock(MessageBusInterface::class);
        $eventBus->expects(self::never())->method('dispatch');

        $component->applyAnonymous(
            request: Request::create('/'),
            entityManager: $this->createMock(EntityManagerInterface::class),
            attendanceFacade: $facade,
            eventBus: $eventBus,
            translator: $this->stubTranslator(),
        );

        self::assertSame('applicationForm.anonymous.phoneOrMobile', $component->anonymousError);
        // Both phone AND mobile inputs must be marked invalid so the rings
        // turn red — either one would have satisfied the rule.
        self::assertTrue($component->getErrorsObject()->has('newParticipant.phone'));
        self::assertTrue($component->getErrorsObject()->has('newParticipant.mobile'));
    }

    public function testRejectsWhenPostalAddressRequiredButIncomplete(): void
    {
        $participant = $this->fullyValidNewParticipant();

        $component = $this->component(requirePostalAddress: true, allowAnonymous: true);
        $component->offer = $this->offer();
        $component->newParticipant = $participant;
        // newPostalAddress is null → incomplete by definition.
        $component->newPostalAddress = null;

        $facade = $this->createMock(AttendanceFacade::class);
        $facade->expects(self::never())->method('createAwaitingEmailConfirmation');

        $component->applyAnonymous(
            request: Request::create('/'),
            entityManager: $this->createMock(EntityManagerInterface::class),
            attendanceFacade: $facade,
            eventBus: $this->createMock(MessageBusInterface::class),
            translator: $this->stubTranslator(),
        );

        self::assertSame('applicationForm.anonymous.postalAddressRequired', $component->anonymousError);
    }

    // ────────────────────────────────────────────────────────────────
    //  Component wiring (test subclass that bypasses Security)
    // ────────────────────────────────────────────────────────────────

    private function component(bool $allowAnonymous = true, bool $requirePostalAddress = false, ?ConsentManager $consentMgr = null): ApplicationForm
    {
        $consentMgr ??= $this->stubConsentManager();
        $reqChecker = $this->createStub(ConsentRequirementChecker::class);
        $reqChecker->method('getMustSignFirstConsents')->willReturn([]);
        $reqChecker->method('hasUnsignedConsents')->willReturn(false);

        $settings = $this->createStub(SystemSettingsManager::class);
        $settings->method('isRequirePostalAddress')->willReturn($requirePostalAddress);

        $appSystems = $this->createStub(ApplicationSystems::class);
        $appSystems->method('findApplicationSystem')->willReturn($this->stubApplicationSystem($allowAnonymous));

        $builder = new ApplicationFormBuilder($consentMgr, $reqChecker, $settings);

        // Anonymous test subclass: short-circuit `getUser()` to null without
        // needing a Security container, and skip the parent's `redirectToRoute`
        // / `addFlash` etc. side effects on the happy path.
        return new class($appSystems, $this->createStub(AttendanceRepository::class), $this->createStub(ParticipantRepositoryInterface::class), $this->createStub(RequestStack::class), $this->createStub(EventDispatcherInterface::class), $settings, $this->createStub(PaymentsFacade::class), $this->createStub(EditionTaskRepository::class), $consentMgr, $reqChecker, $builder, $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), $this->createStub(AttendanceExtraFileRepository::class)) extends ApplicationForm {
            public function getUser(): null
            {
                return null;
            }

            // Skip the LiveComponent ValidatableComponentTrait wiring — these tests
            // exercise the post-validation behavior path; field validators have their
            // own coverage in ApplicationFormBuilderTest.
            public function validate(bool $throw = true): void
            {
            }

            public function validateField(string $propertyName, bool $throw = true): void
            {
            }
        };
    }

    private function stubConsentManager(): ConsentManager
    {
        $consentMgr = $this->createStub(ConsentManager::class);
        $consentMgr->method('forOfferAndParticipants')->willReturn([]);
        $consentMgr->method('getConsentsForOffer')->willReturn([]);

        return $consentMgr;
    }

    private function stubTranslator(): TranslatorInterface
    {
        $translator = $this->createStub(TranslatorInterface::class);
        // Echo the key as the translated string so per-field message assertions
        // can read the underlying key without standing up the real catalogue.
        $translator->method('trans')->willReturnArgument(0);

        return $translator;
    }

    private function stubApplicationSystem(bool $allowAnonymous): ApplicationSystemInterface
    {
        $task = $this->createStub(EditionTask::class);
        $task->method('isAllowAnonymous')->willReturn($allowAnonymous);

        $system = $this->createStub(ApplicationSystemInterface::class);
        $system->method('getTask')->willReturn($task);

        return $system;
    }

    // ────────────────────────────────────────────────────────────────
    //  fixtures
    // ────────────────────────────────────────────────────────────────

    private function offer(): Base
    {
        $offer = new Base();
        $offer->setFee(null);

        return $offer;
    }

    private function fullyValidNewParticipant(): BaseParticipant
    {
        return $this->makeParticipant(
            firstname: 'Anna',
            lastname: 'M.',
            email: 'anna@example.com',
            dob: '2015-06-14',
            mobile: '+491700000000',
        );
    }

    private function makeParticipant(string $firstname, string $lastname, string $email, string $dob, ?string $phone = null, ?string $mobile = null): BaseParticipant
    {
        $participant = new BaseParticipant();
        $participant->setFirstname($firstname);
        $participant->setLastname($lastname);
        $participant->setEmail($email);
        $participant->setDateOfBirth(new \DateTimeImmutable($dob));
        if ($phone) {
            $participant->setPhone($phone);
        }
        if ($mobile) {
            $participant->setMobile($mobile);
        }

        return $participant;
    }
}
