<?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\Components\Consents;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Consent;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Enum\AttendanceTransition;
use Ferienpass\CoreBundle\Repository\ConsentRepository;
use Ferienpass\CoreBundle\Workflow\CompulsoryWithdrawalContext;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\WorkflowInterface;

/**
 * Withdrawing a COMPULSORY consent is a precondition removal: the attendances
 * it gates must be withdrawn alongside it. These tests pin that cascade and its
 * boundaries (scope, mode, and the future-effect guard).
 */
final class ConsentsRevokeCascadeTest extends TestCase
{
    public function testPerParticipantCompulsoryWithdrawsOnlyTheGatedAttendance(): void
    {
        $user = new User();
        $gatedOffer = new Base();
        $otherOffer = new Base();

        $participant = new BaseParticipant();
        $participant->setUser($user);

        $gatedAttendance = $this->attendance($gatedOffer, $participant);
        $untouchedAttendance = $this->attendance($otherOffer, $participant);

        $form = $this->consentForm('compulsory', subject: 'participant', offer: $gatedOffer);
        $consent = new Consent(user: $user, participant: $participant, consentForm: $form, decision: 1);

        // Only the gated offer requires this form.
        $consentManager = $this->createStub(ConsentManager::class);
        $consentManager->method('getConsentsForOffer')
            ->willReturnCallback(static fn (OfferInterface $offer) => $offer === $gatedOffer ? [$form] : []);

        $workflow = $this->createMock(WorkflowInterface::class);
        $workflow->method('can')->willReturn(true);
        // Exactly the gated attendance is withdrawn — and nothing else.
        $workflow->expects(self::once())
            ->method('apply')
            ->with($gatedAttendance, AttendanceTransition::WITHDRAW, ['notify' => false])
            ->willReturn(new Marking());

        $this->component($consent, $user, $consentManager, $workflow)->revoke(1);

        self::assertTrue($consent->isRevoked(), 'The consent itself is always revoked.');
        self::assertSame(AttendanceStatus::CONFIRMED, $untouchedAttendance->getStatus());
    }

    public function testWithdrawalIsSkippedWhenTheWorkflowForbidsIt(): void
    {
        // Mirrors the future-effect rule: the AttendanceGuard blocks withdrawal
        // of already-started offers, so the cascade must leave them alone.
        $user = new User();
        $offer = new Base();
        $participant = new BaseParticipant();
        $participant->setUser($user);
        $this->attendance($offer, $participant);

        $form = $this->consentForm('compulsory', subject: 'participant', offer: $offer);
        $consent = new Consent(user: $user, participant: $participant, consentForm: $form, decision: 1);

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

        $workflow = $this->createMock(WorkflowInterface::class);
        $workflow->method('can')->willReturn(false);
        $workflow->expects(self::never())->method('apply');

        $this->component($consent, $user, $consentManager, $workflow)->revoke(1);

        self::assertTrue($consent->isRevoked());
    }

    public function testNonCompulsoryConsentNeverCascades(): void
    {
        $user = new User();
        $offer = new Base();
        $participant = new BaseParticipant();
        $participant->setUser($user);
        $this->attendance($offer, $participant);

        $form = $this->consentForm('opt-in', subject: 'participant', offer: $offer);
        $consent = new Consent(user: $user, participant: $participant, consentForm: $form, decision: 1);

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

        $workflow = $this->createMock(WorkflowInterface::class);
        $workflow->expects(self::never())->method('can');
        $workflow->expects(self::never())->method('apply');

        $this->component($consent, $user, $consentManager, $workflow)->revoke(1);

        self::assertTrue($consent->isRevoked());
    }

    public function testAccountScopedCompulsoryWithdrawsAcrossAllParticipants(): void
    {
        $user = new User();
        $gatedOffer = new Base();

        $childA = new BaseParticipant();
        $childA->setUser($user);
        $user->addParticipant($childA);
        $attendanceA = $this->attendance($gatedOffer, $childA);

        $childB = new BaseParticipant();
        $childB->setUser($user);
        $user->addParticipant($childB);
        $attendanceB = $this->attendance($gatedOffer, $childB);

        // Account-subject consent (no participant on the signature).
        $form = $this->consentForm('compulsory', subject: 'account', offer: $gatedOffer);
        $consent = new Consent(user: $user, participant: null, consentForm: $form, decision: 1);

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

        $workflow = $this->createMock(WorkflowInterface::class);
        $workflow->method('can')->willReturn(true);
        $applied = [];
        $workflow->expects(self::exactly(2))
            ->method('apply')
            ->willReturnCallback(static function (object $subject) use (&$applied): Marking {
                $applied[] = $subject;

                return new Marking();
            });

        $this->component($consent, $user, $consentManager, $workflow)->revoke(1);

        self::assertContains($attendanceA, $applied);
        self::assertContains($attendanceB, $applied);
    }

    public function testCascadeRunsInsideTheCompulsoryWithdrawalScopeAndResetsAfter(): void
    {
        // The guard waives the convenience no-withdraw rules only while the
        // flag is active, so both can() and apply() must run inside the scope —
        // and it must be reset afterwards so ordinary self-service withdrawals
        // later in the request are unaffected.
        $user = new User();
        $offer = new Base();
        $participant = new BaseParticipant();
        $participant->setUser($user);
        $this->attendance($offer, $participant);

        $form = $this->consentForm('compulsory', subject: 'participant', offer: $offer);
        $consent = new Consent(user: $user, participant: $participant, consentForm: $form, decision: 1);

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

        $context = new CompulsoryWithdrawalContext();
        $activeDuringCan = false;
        $activeDuringApply = false;

        $workflow = $this->createMock(WorkflowInterface::class);
        $workflow->method('can')->willReturnCallback(static function () use ($context, &$activeDuringCan): bool {
            $activeDuringCan = $context->isActive();

            return true;
        });
        $workflow->method('apply')->willReturnCallback(static function () use ($context, &$activeDuringApply): Marking {
            $activeDuringApply = $context->isActive();

            return new Marking();
        });

        $this->component($consent, $user, $consentManager, $workflow, $context)->revoke(1);

        self::assertTrue($activeDuringCan, 'The guard-bypass flag must be active while can() runs.');
        self::assertTrue($activeDuringApply, 'The guard-bypass flag must be active while apply() runs.');
        self::assertFalse($context->isActive(), 'The flag must be reset once the cascade finishes.');
    }

    // ────────────────────────────────────────────────────────────────
    //  wiring
    // ────────────────────────────────────────────────────────────────

    private function component(Consent $signature, User $user, ConsentManager $consentManager, WorkflowInterface $workflow, ?CompulsoryWithdrawalContext $compulsoryWithdrawal = null): Consents
    {
        $consents = $this->createStub(ConsentRepository::class);
        $consents->method('find')->willReturn($signature);

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

        // Test subclass: short-circuit getUser() so no Security container is needed.
        return new class($consents, $consentManager, $entityManager, $this->createStub(Security::class), $workflow, $compulsoryWithdrawal ?? new CompulsoryWithdrawalContext(), $user) extends Consents {
            public function __construct(
                ConsentRepository $consents,
                ConsentManager $consentManager,
                EntityManagerInterface $entityManager,
                Security $security,
                WorkflowInterface $attendanceStateMachine,
                CompulsoryWithdrawalContext $compulsoryWithdrawal,
                private readonly User $testUser,
            ) {
                parent::__construct($consents, $consentManager, $entityManager, $security, $attendanceStateMachine, $compulsoryWithdrawal);
            }

            public function getUser(): ?User
            {
                return $this->testUser;
            }
        };
    }

    private function attendance(OfferInterface $offer, BaseParticipant $participant): Attendance
    {
        $attendance = new Attendance($offer, $participant);
        $attendance->setStatus(AttendanceStatus::CONFIRMED);
        $participant->addAttendance($attendance);

        return $attendance;
    }

    private function consentForm(string $compulsory, string $subject, ?OfferInterface $offer = null): ConsentForm
    {
        $form = new ConsentForm();
        $form->setCompulsory($compulsory);
        $form->setSubject($subject);
        $form->setOffer($offer);

        return $form;
    }
}
