<?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\SignIn;

use Ferienpass\CmsBundle\Components\SignIn\Registration;
use Ferienpass\CoreBundle\Entity\Consent;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\ConsentRepository;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use PHPUnit\Framework\TestCase;

/**
 * Scenario 4: anonymous applicant later registers an account.
 *
 * Before registration:
 *   - Participant rows exist with `user = NULL` and a known UUID.
 *   - Consent rows exist tied to those participants with `user = NULL`
 *     (decisions captured during the anonymous apply step).
 *
 * After `Registration::adoptStoredParticipants` runs:
 *   - The matching orphan participants get their user bound to the new account.
 *   - The Consent rows tied to those participants ALSO get their user bound,
 *     so post-registration audit treats them as account-owned.
 */
final class RegistrationAdoptionTest extends TestCase
{
    public function testAdoptsOrphanParticipantsAndTheirConsents(): void
    {
        $newUser = $this->user();

        // Orphan participants (user=null) that this browser created anonymously.
        $p1 = new BaseParticipant();
        $p2 = new BaseParticipant();

        // Orphan consent rows tied to those participants.
        $consentP1A = new Consent(user: null, participant: $p1, consentForm: $this->consentForm());
        $consentP1B = new Consent(user: null, participant: $p1, consentForm: $this->consentForm());
        $consentP2 = new Consent(user: null, participant: $p2, consentForm: $this->consentForm());

        $participants = $this->createMock(ParticipantRepositoryInterface::class);
        $participants->expects(self::once())
            ->method('findBy')
            ->with(['uuid' => [(string) $p1->getUuid(), (string) $p2->getUuid()], 'user' => null])
            ->willReturn([$p1, $p2]);

        $consents = $this->createMock(ConsentRepository::class);
        $consents->expects(self::once())
            ->method('findBy')
            ->with(['participant' => [$p1, $p2], 'user' => null])
            ->willReturn([$consentP1A, $consentP1B, $consentP2]);

        $registration = new Registration($participants, $consents);
        $registration->participantIds = [(string) $p1->getUuid(), (string) $p2->getUuid()];

        $this->invokeAdopt($registration, $newUser);

        // Participants now belong to the new user.
        self::assertSame($newUser, $p1->getUser(), 'P1 must be adopted.');
        self::assertSame($newUser, $p2->getUser(), 'P2 must be adopted.');

        // Consents now belong to the new user too.
        self::assertSame($newUser, $consentP1A->getUser(), 'P1 consent A must be adopted.');
        self::assertSame($newUser, $consentP1B->getUser(), 'P1 consent B must be adopted.');
        self::assertSame($newUser, $consentP2->getUser(), 'P2 consent must be adopted.');
    }

    public function testNoOpWhenNoParticipantIdsStored(): void
    {
        $participants = $this->createMock(ParticipantRepositoryInterface::class);
        $participants->expects(self::never())->method('findBy');

        $consents = $this->createMock(ConsentRepository::class);
        $consents->expects(self::never())->method('findBy');

        $registration = new Registration($participants, $consents);
        $registration->participantIds = [];

        $this->invokeAdopt($registration, $this->user());
    }

    public function testNoOpWhenStoredIdsDoNotMatchAnyOrphanParticipant(): void
    {
        // A stale or forged UUID arrives via localStorage but doesn't match any
        // still-orphan participant — adoption silently bails before touching
        // consents (so a non-existent participant's consents can't be claimed).
        $participants = $this->createMock(ParticipantRepositoryInterface::class);
        $participants->expects(self::once())
            ->method('findBy')
            ->willReturn([]);

        $consents = $this->createMock(ConsentRepository::class);
        $consents->expects(self::never())
            ->method('findBy', 'No participants adopted → no consent query.');

        $registration = new Registration($participants, $consents);
        $registration->participantIds = ['01975c4d-c1f8-7000-8000-000000000000'];

        $this->invokeAdopt($registration, $this->user());
    }

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

    private function user(): User
    {
        $user = new User();
        $user->setEmail('newly-registered@example.com');

        return $user;
    }

    private function consentForm(): ConsentForm
    {
        return $this->createStub(ConsentForm::class);
    }

    /**
     * Registration::adoptStoredParticipants is private — invoke it directly so
     * we can assert on the side effects without standing up the full submit()
     * action (which dispatches events, builds redirects, etc.).
     */
    private function invokeAdopt(Registration $registration, User $user): void
    {
        $ref = new \ReflectionMethod($registration, 'adoptStoredParticipants');
        $ref->invoke($registration, $user);
    }
}
