<?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\CoreBundle\Tests\Application;

use Ferienpass\CoreBundle\Application\ApplicationFormBuilder;
use Ferienpass\CoreBundle\Application\ApplicationFormSpec;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentRequirementChecker;
use Ferienpass\CoreBundle\ConsentManager\ConsentState;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\PostalAddress;
use Ferienpass\CoreBundle\Settings\SystemSettingsManager;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Uuid;

final class ApplicationFormBuilderTest extends TestCase
{
    // ────────────────────────────────────────────────────────────────
    //  validateParticipantData
    // ────────────────────────────────────────────────────────────────

    public function testValidateParticipantDataAcceptsFullyValid(): void
    {
        $participant = $this->newParticipant('Anna', 'M.', 'anna@example.com', '2015-06-14', mobile: '+491700000000');

        $errors = $this->builder()->validateParticipantData($participant);

        self::assertSame([], $errors);
    }

    public function testValidateParticipantDataFlagsMissingFirstname(): void
    {
        $participant = $this->newParticipant('', 'M.', 'anna@example.com', '2015-06-14', mobile: '+491700000000');

        self::assertSame(
            ['firstname' => 'applicationForm.anonymous.fieldRequired'],
            $this->builder()->validateParticipantData($participant),
        );
    }

    public function testValidateParticipantDataFlagsMissingLastname(): void
    {
        $participant = $this->newParticipant('Anna', '', 'anna@example.com', '2015-06-14', mobile: '+491700000000');

        self::assertSame(
            ['lastname' => 'applicationForm.anonymous.fieldRequired'],
            $this->builder()->validateParticipantData($participant),
        );
    }

    public function testValidateParticipantDataFlagsMissingDob(): void
    {
        $participant = $this->newParticipant('Anna', 'M.', 'anna@example.com', null, mobile: '+491700000000');

        self::assertSame(
            ['dateOfBirth' => 'applicationForm.anonymous.fieldRequired'],
            $this->builder()->validateParticipantData($participant),
        );
    }

    public function testValidateParticipantDataFlagsMissingEmail(): void
    {
        $participant = $this->newParticipant('Anna', 'M.', null, '2015-06-14', mobile: '+491700000000');

        self::assertSame(
            ['email' => 'applicationForm.anonymous.fieldRequired'],
            $this->builder()->validateParticipantData($participant),
        );
    }

    public function testValidateParticipantDataRequiresPhoneOrMobile(): void
    {
        $participant = $this->newParticipant('Anna', 'M.', 'anna@example.com', '2015-06-14');

        // Both phone *and* mobile flagged so both rings turn red — either one
        // would satisfy the rule. The SHORT per-field key is rendered inline
        // under each input; the component picks the long-form banner.
        self::assertSame(
            [
                'phone' => 'applicationForm.anonymous.fieldPhoneOrMobile',
                'mobile' => 'applicationForm.anonymous.fieldPhoneOrMobile',
            ],
            $this->builder()->validateParticipantData($participant),
        );
    }

    public function testValidateParticipantDataAcceptsPhoneAloneAsContact(): void
    {
        $participant = $this->newParticipant('Anna', 'M.', 'anna@example.com', '2015-06-14', phone: '+49301234567');

        self::assertSame([], $this->builder()->validateParticipantData($participant));
    }

    public function testValidateParticipantDataReturnsAllInvalidFieldsKeyed(): void
    {
        $participant = $this->newParticipant('', '', null, null);

        // Every invalid field shows up with its SHORT per-field translation
        // key so the component can paint individual red rings + render a
        // succinct paragraph under each input.
        self::assertSame(
            [
                'firstname' => 'applicationForm.anonymous.fieldRequired',
                'lastname' => 'applicationForm.anonymous.fieldRequired',
                'dateOfBirth' => 'applicationForm.anonymous.fieldRequired',
                'email' => 'applicationForm.anonymous.fieldRequired',
                'phone' => 'applicationForm.anonymous.fieldPhoneOrMobile',
                'mobile' => 'applicationForm.anonymous.fieldPhoneOrMobile',
            ],
            $this->builder()->validateParticipantData($participant),
        );
    }

    // ────────────────────────────────────────────────────────────────
    //  postalAddressComplete
    // ────────────────────────────────────────────────────────────────

    public function testPostalAddressCompleteAllowsNull(): void
    {
        self::assertFalse($this->builder()->postalAddressComplete(null));
    }

    public function testPostalAddressCompleteRequiresStreet(): void
    {
        $address = $this->newAddress(street: null, postalCode: '48529', city: 'Nordhorn');
        self::assertFalse($this->builder()->postalAddressComplete($address));
    }

    public function testPostalAddressCompleteRequiresPostalCode(): void
    {
        $address = $this->newAddress(street: 'Hauptstraße 1', postalCode: null, city: 'Nordhorn');
        self::assertFalse($this->builder()->postalAddressComplete($address));
    }

    public function testPostalAddressCompleteRequiresCity(): void
    {
        $address = $this->newAddress(street: 'Hauptstraße 1', postalCode: '48529', city: null);
        self::assertFalse($this->builder()->postalAddressComplete($address));
    }

    public function testPostalAddressCompleteAcceptsMinimumComplete(): void
    {
        $address = $this->newAddress(street: 'Hauptstraße 1', postalCode: '48529', city: 'Nordhorn');
        self::assertTrue($this->builder()->postalAddressComplete($address));
    }

    // ────────────────────────────────────────────────────────────────
    //  consentsResolved
    // ────────────────────────────────────────────────────────────────

    public function testConsentsResolvedTrueWhenSpecHasNoConsents(): void
    {
        $offer = $this->createStub(Base::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $reqChecker = $this->createStub(ConsentRequirementChecker::class);
        $reqChecker->method('getMustSignFirstConsents')->willReturn([]);
        $consentMgr = $this->createStub(ConsentManager::class);
        $consentMgr->method('forOfferAndParticipants')->willReturn([]);
        $consentMgr->method('getConsentsForOffer')->willReturn([]);
        $settings = $this->createStub(SystemSettingsManager::class);

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

        self::assertTrue($builder->consentsResolved($offer, $participant, null, []));
    }

    public function testConsentsResolvedRequiresAgreeForCompulsoryForm(): void
    {
        // Single compulsory form on the offer, not yet decided → not resolved.
        $formId = Uuid::v7();
        $form = $this->compulsoryForm($formId);
        $state = $this->newState($form, outstanding: true);

        $builder = $this->builderWith(
            participantConsentsForOffer: [$state],
        );

        $offer = $this->createStub(Base::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $key = (string) $formId;

        self::assertFalse(
            $builder->consentsResolved($offer, $participant, null, []),
            'No decision should leave a compulsory consent unresolved.',
        );

        self::assertFalse(
            $builder->consentsResolved($offer, $participant, null, [$key => '0']),
            'A declined compulsory consent is unresolved.',
        );

        self::assertTrue(
            $builder->consentsResolved($offer, $participant, null, [$key => '1']),
            'An agreed compulsory consent is resolved.',
        );
    }

    public function testConsentsResolvedForceDecisionAcceptsEitherYesOrNo(): void
    {
        $formId = Uuid::v7();
        $form = $this->forceDecisionForm($formId);
        $state = $this->newState($form, outstanding: true);

        $builder = $this->builderWith(participantConsentsForOffer: [$state]);

        $offer = $this->createStub(Base::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $key = (string) $formId;

        self::assertFalse($builder->consentsResolved($offer, $participant, null, []));
        self::assertTrue($builder->consentsResolved($offer, $participant, null, [$key => '0']));
        self::assertTrue($builder->consentsResolved($offer, $participant, null, [$key => '1']));
    }

    // ────────────────────────────────────────────────────────────────
    //  build()
    // ────────────────────────────────────────────────────────────────

    public function testBuildFlagsNeedsParticipantDataWhenParticipantIsNull(): void
    {
        // The builder reads getMinAge/getMaxAge which live on Base, not on
        // the OfferInterface — stub the concrete class.
        $offer = $this->createStub(Base::class);
        $offer->method('getApplicationExtra')->willReturn(null);
        $offer->method('getMinAge')->willReturn(8);
        $offer->method('getMaxAge')->willReturn(12);

        $reqChecker = $this->createStub(ConsentRequirementChecker::class);
        $reqChecker->method('getMustSignFirstConsents')->willReturn([]);

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

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

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

        $spec = $builder->build($offer, null, null);

        self::assertInstanceOf(ApplicationFormSpec::class, $spec);
        self::assertTrue($spec->needsParticipantData);
        self::assertTrue($spec->requiresPostalAddress);
        self::assertSame(8, $spec->minAge);
        self::assertSame(12, $spec->maxAge);
        self::assertSame([], $spec->participantConsents, 'No participant given → no participant-level states.');
    }

    public function testBuildExposesParticipantLevelConsentsForAnonymousPreCreate(): void
    {
        // Anonymous applicant — participant is null but the offer has a non-user-level form.
        $form = $this->compulsoryForm(Uuid::v7());
        $offer = $this->createStub(Base::class);
        $offer->method('getApplicationExtra')->willReturn(null);
        $offer->method('getMinAge')->willReturn(null);
        $offer->method('getMaxAge')->willReturn(null);

        $reqChecker = $this->createStub(ConsentRequirementChecker::class);
        $reqChecker->method('getMustSignFirstConsents')->willReturn([]);

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

        $settings = $this->createStub(SystemSettingsManager::class);

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

        $spec = $builder->build($offer, null, null);

        self::assertCount(1, $spec->participantConsents);
        self::assertSame($form, $spec->participantConsents[0]->getConsentForm());
    }

    // ────────────────────────────────────────────────────────────────
    //  helpers
    // ────────────────────────────────────────────────────────────────

    private function builder(): ApplicationFormBuilder
    {
        return new ApplicationFormBuilder(
            $this->createStub(ConsentManager::class),
            $this->createStub(ConsentRequirementChecker::class),
            $this->createStub(SystemSettingsManager::class),
        );
    }

    private function builderWith(array $participantConsentsForOffer = []): ApplicationFormBuilder
    {
        $consentMgr = $this->createStub(ConsentManager::class);
        $consentMgr->method('forOfferAndParticipants')->willReturn($participantConsentsForOffer);
        // For the pre-create (no participant) branch:
        $consentMgr->method('getConsentsForOffer')->willReturn(
            array_map(static fn (ConsentState $s) => $s->getConsentForm(), $participantConsentsForOffer),
        );

        return new ApplicationFormBuilder(
            $consentMgr,
            $this->createStub(ConsentRequirementChecker::class),
            $this->createStub(SystemSettingsManager::class),
        );
    }

    private function newParticipant(string $firstname, string $lastname, ?string $email, ?string $dobYmd, ?string $phone = null, ?string $mobile = null): ParticipantInterface
    {
        $stub = $this->createStub(ParticipantInterface::class);
        $stub->method('getFirstname')->willReturn($firstname);
        $stub->method('getLastname')->willReturn($lastname);
        $stub->method('getEmail')->willReturn($email);
        $stub->method('getDateOfBirth')->willReturn($dobYmd ? new \DateTimeImmutable($dobYmd) : null);
        $stub->method('getPhone')->willReturn($phone);
        $stub->method('getMobile')->willReturn($mobile);

        return $stub;
    }

    private function newAddress(?string $street, ?string $postalCode, ?string $city): PostalAddress
    {
        $address = new PostalAddress();
        $address->setStreet($street);
        $address->setPostalCode($postalCode);
        $address->setCity($city);

        return $address;
    }

    private function compulsoryForm(Uuid $id): ConsentForm
    {
        $form = $this->createStub(ConsentForm::class);
        $form->method('getId')->willReturn($id);
        $form->method('isUserLevel')->willReturn(false);
        $form->method('getCompulsory')->willReturn('compulsory');

        return $form;
    }

    private function forceDecisionForm(Uuid $id): ConsentForm
    {
        $form = $this->createStub(ConsentForm::class);
        $form->method('getId')->willReturn($id);
        $form->method('isUserLevel')->willReturn(false);
        $form->method('getCompulsory')->willReturn('force_decision');

        return $form;
    }

    private function newState(ConsentForm $form, bool $outstanding): ConsentState
    {
        $state = $this->createStub(ConsentState::class);
        $state->method('getConsentForm')->willReturn($form);
        $state->method('isOutstanding')->willReturn($outstanding);

        return $state;
    }
}
