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

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentState;
use Ferienpass\CoreBundle\Entity\Consent;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\ConsentText;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\ConsentFormRepository;
use Ferienpass\CoreBundle\Repository\ConsentRepository;
use PHPUnit\Framework\TestCase;

final class ConsentManagerTest extends TestCase
{
    public function testSignReturnsFreshState(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createMock(ConsentRepository::class);
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $user = $this->createStub(User::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $consentForm = $this->createStub(ConsentForm::class);
        $consentForm->method('getText')->willReturn('Test text');

        $consentRepository->method('findOneBy')->willReturn(null);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $state = $manager->sign($consentForm, $user, $participant);

        $this->assertInstanceOf(ConsentState::class, $state);
        $this->assertSame($consentForm, $state->getConsentForm());
        $this->assertSame($participant, $state->getParticipant());
        $this->assertSame($user, $state->getUser());
        $this->assertTrue($state->isSigned());
    }

    public function testDeclineReturnsFreshState(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createMock(ConsentRepository::class);
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $user = $this->createStub(User::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $consentForm = $this->createStub(ConsentForm::class);
        $consentForm->method('getText')->willReturn('Test text');

        $consentRepository->method('findOneBy')->willReturn(null);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $state = $manager->decline($consentForm, $user, $participant);

        $this->assertInstanceOf(ConsentState::class, $state);
        $this->assertSame($consentForm, $state->getConsentForm());
        $this->assertSame($participant, $state->getParticipant());
        $this->assertSame($user, $state->getUser());
        $this->assertTrue($state->isRejected());
    }

    public function testSignReusesExistingConsent(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createMock(ConsentRepository::class);
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $user = $this->createStub(User::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $consentForm = $this->createStub(ConsentForm::class);
        $consentForm->method('getText')->willReturn('Test text');

        $existingConsent = new Consent($user, participant: $participant, consentForm: $consentForm);

        $consentRepository->expects($this->once())
            ->method('findOneBy')
            ->with([
                'consentForm' => $consentForm,
                'user' => $user,
                'participant' => $participant,
            ])
            ->willReturn($existingConsent);

        $entityManager->expects($this->never())->method('persist');
        $entityManager->expects($this->once())->method('flush');

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $manager->sign($consentForm, $user, $participant);
    }

    public function testDeclineReusesExistingConsent(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createMock(ConsentRepository::class);
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $user = $this->createStub(User::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $consentForm = $this->createStub(ConsentForm::class);
        $consentForm->method('getText')->willReturn('Test text');

        $existingConsent = new Consent($user, participant: $participant, consentForm: $consentForm);

        $consentRepository->expects($this->once())
            ->method('findOneBy')
            ->with([
                'consentForm' => $consentForm,
                'user' => $user,
                'participant' => $participant,
            ])
            ->willReturn($existingConsent);

        $entityManager->expects($this->never())->method('persist');
        $entityManager->expects($this->once())->method('flush');

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $manager->decline($consentForm, $user, $participant);
    }

    public function testSignCreatesNewConsentIfTextHasChanged(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createMock(ConsentRepository::class);
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $user = $this->createStub(User::class);
        $participant = $this->createStub(ParticipantInterface::class);
        $consentForm = $this->createStub(ConsentForm::class);
        $consentForm->method('getText')->willReturn('New text');

        $oldConsentText = new ConsentText('Old text');
        $existingConsent = new Consent($user, participant: $participant, consentText: $oldConsentText);

        $consentRepository->expects($this->once())
            ->method('findOneBy')
            ->willReturn($existingConsent);

        $entityManager->expects($this->once())->method('persist');
        $entityManager->expects($this->once())->method('flush');

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $manager->sign($consentForm, $user, $participant);
    }

    public function testGetConsentsForOffer(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createStub(ConsentRepository::class);
        $entityManager = $this->createStub(EntityManagerInterface::class);

        $offer = $this->createStub(OfferInterface::class);
        $consentForm = $this->createStub(ConsentForm::class);

        $consentFormRepository->method('findForOffer')->willReturnCallback(static fn ($o) => $o === $offer ? [$consentForm] : []);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $result = $manager->getConsentsForOffer($offer);
        $this->assertCount(1, $result);
        $this->assertSame($consentForm, $result[0]);

        $otherOffer = $this->createStub(OfferInterface::class);
        $this->assertSame([], $manager->getConsentsForOffer($otherOffer));
    }

    public function testFindStateReturnsConsentState(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createStub(ConsentRepository::class);
        $entityManager = $this->createStub(EntityManagerInterface::class);

        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('current text');

        $consentRepository->method('findSignatures')->willReturn([]);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $state = $manager->findState($consentForm, $user);

        self::assertSame($consentForm, $state->getConsentForm());
        self::assertFalse($state->isSigned());
        self::assertFalse($state->isAcknowledged());
    }

    public function testFindStateReturnsSignedStateWhenValidSignatureExists(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createStub(ConsentRepository::class);
        $entityManager = $this->createStub(EntityManagerInterface::class);

        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('current text');

        $signature = new Consent($user, consentForm: $consentForm, decision: 1);

        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $state = $manager->findState($consentForm, $user);

        self::assertTrue($state->isSigned());
        self::assertTrue($state->isAcknowledged());
    }

    public function testFindStatePrefersValidSignatureOverInvalidOne(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createStub(ConsentRepository::class);
        $entityManager = $this->createStub(EntityManagerInterface::class);

        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('new text');

        // Old signature with outdated text
        $oldText = new ConsentText('old text');
        $invalidSignature = new Consent($user, consentText: $oldText, decision: 1);

        // Valid signature with current text
        $currentText = new ConsentText('new text');
        $validSignature = new Consent($user, consentText: $currentText, decision: 1);

        $consentRepository->method('findSignatures')->willReturn([$invalidSignature, $validSignature]);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $state = $manager->findState($consentForm, $user);

        // Should pick the valid signature
        self::assertTrue($state->isSigned());
    }

    public function testForOfferAndParticipantsReturnsEmptyWhenNoForms(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([]);

        $entityManager = $this->createStub(EntityManagerInterface::class);

        $offer = $this->createStub(OfferInterface::class);
        $user = new User();

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $states = $manager->forOfferAndParticipants($offer, [], $user);

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

    public function testFindStatePicksNewerSignatureWhenBothValidSameText(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createStub(ConsentRepository::class);
        $entityManager = $this->createStub(EntityManagerInterface::class);

        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('same text');

        $consentText = new ConsentText('same text');

        $older = new Consent($user, consentText: $consentText, decision: 1);
        // Inject a slightly later createdAt via reflection to make $newer > $older
        $newer = new Consent($user, consentText: $consentText, decision: 1);
        $prop = new \ReflectionProperty(Consent::class, 'createdAt');
        $prop->setValue($older, new \DateTimeImmutable('2024-01-01 10:00:00'));
        $prop->setValue($newer, new \DateTimeImmutable('2024-01-01 11:00:00'));

        // Return old first, new second — newer should win via elseif branch
        $consentRepository->method('findSignatures')->willReturn([$older, $newer]);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $state = $manager->findState($consentForm, $user);

        self::assertSame($newer, $state->getSignature());
    }

    public function testFindStateSkipsSignatureFromDifferentUser(): void
    {
        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createStub(ConsentRepository::class);
        $entityManager = $this->createStub(EntityManagerInterface::class);

        $user = new User();
        $otherUser = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('text');

        // Signature belongs to $otherUser, not $user
        $signature = new Consent($otherUser, consentForm: $consentForm, decision: 1);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $state = $manager->findState($consentForm, $user);

        // otherUser !== user → signature skipped → no best
        self::assertFalse($state->isSigned());
    }

    public function testForOfferAndParticipantsReturnStatePerFormWithNoParticipants(): void
    {
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([]);

        $entityManager = $this->createStub(EntityManagerInterface::class);

        $offer = $this->createStub(OfferInterface::class);
        $user = new User();

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $states = $manager->forOfferAndParticipants($offer, [], $user);

        self::assertCount(1, $states);
        self::assertInstanceOf(ConsentState::class, $states[0]);
    }

    public function testForOfferAndParticipantsReturnsSingleStateForAccountScopeWithMultipleParticipants(): void
    {
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('account');

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([]);

        $entityManager = $this->createStub(EntityManagerInterface::class);

        $offer = $this->createStub(OfferInterface::class);
        $user = new User();
        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant2 = $this->createStub(ParticipantInterface::class);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $states = $manager->forOfferAndParticipants($offer, [$participant1, $participant2], $user);

        self::assertCount(1, $states);
    }

    public function testForOfferAndParticipantsReturnsSignedStateForAccountScopeWithMultipleParticipants(): void
    {
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('account');
        $consentForm->setCompulsory('compulsory');

        $consentForm2 = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm2->setText('some text 2');
        $consentForm2->setSubject('account');
        $consentForm2->setCompulsory('compulsory');

        $user = new User();
        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getUser')->willReturn($user);
        $participant2 = $this->createStub(ParticipantInterface::class);
        $participant2->method('getUser')->willReturn($user);

        $signature = new Consent($user, null, null, null, $consentForm, null, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm, $consentForm2]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $offer = $this->createStub(OfferInterface::class);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $states = $manager->forOfferAndParticipants($offer, [$participant1, $participant2], $user);

        self::assertCount(2, $states);
        self::assertTrue($states[0]->isSigned());
        self::assertNull($states[0]->getParticipant());
        self::assertFalse($states[1]->isSigned());
        self::assertNull($states[1]->getParticipant());
    }

    public function testAccountScopedConsentSignatureFoundWithMultipleParticipants(): void
    {
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('account');
        $consentForm->setCompulsory('compulsory');

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getUser')->willReturn($user);
        $participant1->method('getId')->willReturn(1);
        $participant2 = $this->createStub(ParticipantInterface::class);
        $participant2->method('getUser')->willReturn($user);
        $participant2->method('getId')->willReturn(2);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $signature = new Consent($user, null, null, null, $consentForm, null, 1);
        // Ensure signature has participant = null
        self::assertNull($signature->getParticipant());

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $offer = $this->createStub(OfferInterface::class);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);
        $states = $manager->forOfferAndParticipants($offer, [$participant1, $participant2], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned(), 'Account-scoped consent should be signed');
        self::assertNull($states[0]->getParticipant());
    }

    public function testAccountScopedConsentSignatureFoundByForOfferAndParticipantsAfterSigning(): void
    {
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('account');
        $consentForm->setCompulsory('compulsory');

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getUser')->willReturn($user);
        $participant1->method('getId')->willReturn(1);

        $consentFormRepository = $this->createMock(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createMock(ConsentRepository::class);
        // This is tricky because we use a real repo in reality, but here we're mocking.
        // We'll simulate the state by capturing the sign call.
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $savedSignatures = [];
        $entityManager->method('persist')->willReturnCallback(static function ($entity) use (&$savedSignatures) {
            if ($entity instanceof Consent) {
                $savedSignatures[] = $entity;
            }
        });

        // Sign the consent for the user and NULL participant (account scope)
        $manager->sign($consentForm, $user, null);

        self::assertCount(1, $savedSignatures);
        $signature = $savedSignatures[0];
        self::assertNull($signature->getParticipant());
        self::assertSame($user, $signature->getUser());
        self::assertSame($consentForm, $signature->getConsentForm());

        // Now mock the repository to return our saved signature
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [$participant1], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned(), 'The state should be signed');
        self::assertNull($states[0]->getParticipant(), 'Participant should be null for account scope');
    }

    public function testAccountScopedConsentSignatureFoundEvenIfStoredWithParticipantId(): void
    {
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('account');

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getId')->willReturn(123);

        // A signature that incorrectly has a participant ID but is for an account-scoped form
        $signature = new Consent($user, null, null, $participant1, $consentForm, null, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [$participant1], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned(), 'Should be signed even if stored with participant ID');
    }

    public function testAccountScopedConsentSignatureFoundEvenIfStoredWithAnotherParticipantId(): void
    {
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('account');

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getId')->willReturn(123);
        $participant1->method('getUser')->willReturn($user);

        $otherParticipant = $this->createStub(ParticipantInterface::class);
        $otherParticipant->method('getId')->willReturn(456);
        $otherParticipant->method('getUser')->willReturn($user);

        // A signature stored with a participant ID that is NOT in the current participants list
        $signature = new Consent($user, null, null, $otherParticipant, $consentForm, null, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        // Only participant1 is in the list
        $states = $manager->forOfferAndParticipants($offer, [$participant1], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned(), 'Should be signed even if stored with a DIFFERENT participant ID');
        self::assertNull($states[0]->getParticipant());
    }

    public function testOfferLevelParticipantScopedConsentSignatureFound(): void
    {
        $user = new User();
        $offer = $this->createStub(OfferInterface::class);
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('participant');
        // Offer-level consent
        $consentForm->setOffer($offer);

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getId')->willReturn(123);
        $participant1->method('getUser')->willReturn($user);

        // Signature for participant1
        $signature = new Consent($user, null, null, $participant1, $consentForm, null, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $states = $manager->forOfferAndParticipants($offer, [$participant1], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned(), 'Offer-level participant-scoped consent should be signed');
        self::assertSame($participant1, $states[0]->getParticipant());
    }

    public function testEditionLevelParticipantScopedConsentShownOncePerParticipant(): void
    {
        // Cardinality is driven by the subject, not by edition-vs-offer placement:
        // a participant-scoped consent is collected once PER participant even when
        // it is edition-scoped (offer === null) and rendered in the step-1 gate.
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('participant');
        $consentForm->setOffer(null); // Edition-level

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getId')->willReturn(111);
        $participant2 = $this->createStub(ParticipantInterface::class);
        $participant2->method('getId')->willReturn(222);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [$participant1, $participant2], $user);

        self::assertCount(2, $states);
        self::assertSame($participant1, $states[0]->getParticipant());
        self::assertSame($participant2, $states[1]->getParticipant());
    }

    public function testEditionLevelParticipantScopedConsentMatchedPerParticipant(): void
    {
        // An edition-level participant-scoped consent is signed independently per
        // participant: a signature for one child does not satisfy it for another.
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('some text');
        $consentForm->setSubject('participant');
        $consentForm->setOffer(null);

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getId')->willReturn(111);
        $participant1->method('getUser')->willReturn($user);
        $participant2 = $this->createStub(ParticipantInterface::class);
        $participant2->method('getId')->willReturn(222);
        $participant2->method('getUser')->willReturn($user);

        // Signature for participant 2 only
        $signature = new Consent($user, null, null, $participant2, $consentForm, null, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [$participant1, $participant2], $user);

        self::assertCount(2, $states);
        self::assertSame($participant1, $states[0]->getParticipant());
        self::assertFalse($states[0]->isSigned(), 'participant1 has no signature of their own');
        self::assertSame($participant2, $states[1]->getParticipant());
        self::assertTrue($states[1]->isSigned(), 'participant2 signed for themselves');
    }

    public function testEditionLevelParticipantScopedConsentFoundByHashIfSignedForAnotherForm(): void
    {
        $user = new User();
        $text = 'Shared Text';
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText($text);
        $consentForm->setSubject('participant');
        $consentForm->setOffer(null); // Edition-level

        $offerForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $offerForm->setText($text);
        $offerForm->setSubject('participant');
        $offerForm->setOffer($this->createStub(OfferInterface::class)); // Offer-level

        $participant = $this->createStub(ParticipantInterface::class);
        $participant->method('getUser')->willReturn($user);

        // Signature for the OFFER-level form
        $signature = new Consent($user, null, null, $participant, $offerForm, null, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [$participant], $user);

        // The edition-level form is also in the list?
        // Let's assume forOfferAndParticipants is called with a list that includes the edition-level form.
        $states = $manager->forFormsAndParticipants([$consentForm], [$participant], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned(), 'Edition-level participant consent should be found by hash if an offer-level one with same text was signed');
    }

    public function testSignatureNotPickedUpIfTextMismatch(): void
    {
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('Current Text');
        $consentForm->setSubject('account');

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getUser')->willReturn($user);

        // If I want to simulate a signature for OLD text of the SAME form:
        $oldText = new ConsentText('Old Text');
        $signature = new Consent($user, null, null, null, $consentForm, $oldText, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [$participant1], $user);

        self::assertCount(1, $states);
        self::assertFalse($states[0]->isSigned(), 'Signature with old text should not be considered signed');
    }

    public function testAccountScopedConsentPickedUpByMultipleParticipantsApplication(): void
    {
        $user = new User();
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText('text');
        $consentForm->setSubject('account');

        $participant1 = $this->createStub(ParticipantInterface::class);
        $participant1->method('getId')->willReturn(1);
        $participant1->method('getUser')->willReturn($user);
        $participant2 = $this->createStub(ParticipantInterface::class);
        $participant2->method('getId')->willReturn(2);
        $participant2->method('getUser')->willReturn($user);

        // Signature with NULL participant
        $signature = new Consent($user, null, null, null, $consentForm, null, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [$participant1, $participant2], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned());
        self::assertNull($states[0]->getParticipant());
    }

    public function testSignatureFoundByHashEvenIfFormMismatch(): void
    {
        $user = new User();
        $text = 'Same Text';
        $consentForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $consentForm->setText($text);
        $consentForm->setSubject('account');

        $consentText = new ConsentText($text);
        $consentForm->setConsentText($consentText);

        $anotherForm = new ConsentForm(\Symfony\Component\Uid\Uuid::v7());
        $anotherForm->setText($text);
        $anotherForm->setConsentText($consentText);

        // Signature associated with ANOTHER form, but SAME text hash
        $signature = new Consent($user, null, null, null, $anotherForm, $consentText, 1);

        $consentFormRepository = $this->createStub(ConsentFormRepository::class);
        $consentFormRepository->method('findForOffer')->willReturn([$consentForm]);

        $consentRepository = $this->createStub(ConsentRepository::class);
        // findSignatures will return signatures based on forms OR hashes
        $consentRepository->method('findSignatures')->willReturn([$signature]);

        $entityManager = $this->createStub(EntityManagerInterface::class);
        $manager = new ConsentManager($consentFormRepository, $consentRepository, $entityManager);

        $offer = $this->createStub(OfferInterface::class);
        $states = $manager->forOfferAndParticipants($offer, [], $user);

        self::assertCount(1, $states);
        self::assertTrue($states[0]->isSigned(), 'Signature should be found by hash even if associated with a different form');
    }
}
