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

use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Facade\EraseDataFacade;
use Ferienpass\CoreBundle\Repository\ParticipantRepository;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use Ferienpass\CoreBundle\Settings\SystemSettingsManager;
use PHPUnit\Framework\TestCase;

class EraseDataFacadeTest extends TestCase
{
    public function testExpiredParticipantsWithNullPeriod(): void
    {
        $connection = $this->createMock(Connection::class);
        $participants = $this->createMock(ParticipantRepositoryInterface::class);
        $doctrine = $this->createMock(EntityManagerInterface::class);
        $systemSettingsManager = $this->createMock(SystemSettingsManager::class);

        $systemSettingsManager->expects($this->once())
            ->method('getEraseDataPeriod')
            ->willReturn(null);

        $facade = new EraseDataFacade($connection, $participants, $doctrine, $systemSettingsManager, 'App\Entity\Participant');

        $this->assertSame([], $facade->expiredParticipants());
    }

    public function testExpiredParticipantsWithValidPeriod(): void
    {
        $connection = $this->createMock(Connection::class);
        $participants = $this->createMock(ParticipantRepositoryInterface::class);
        $doctrine = $this->createMock(EntityManagerInterface::class);
        $systemSettingsManager = $this->createMock(SystemSettingsManager::class);

        $systemSettingsManager->expects($this->once())
            ->method('getEraseDataPeriod')
            ->willReturn('30 days');

        $qb = $this->createMock(QueryBuilder::class);
        $expr = new Query\Expr();

        $doctrine->method('createQueryBuilder')->willReturn($qb);
        $qb->method('select')->willReturn($qb);
        $qb->method('from')->willReturn($qb);
        $qb->method('leftJoin')->willReturn($qb);
        $qb->method('innerJoin')->willReturn($qb);
        $qb->method('groupBy')->willReturn($qb);
        $qb->method('setParameter')->willReturn($qb);
        $qb->method('where')->willReturn($qb);
        $qb->method('orderBy')->willReturn($qb);
        $qb->method('expr')->willReturn($expr);

        $query = $this->getMockBuilder(Query::class)
            ->disableOriginalConstructor()
            ->getMock();
        $qb->method('getQuery')->willReturn($query);
        $query->method('getScalarResult')->willReturn([]);

        $facade = new EraseDataFacade($connection, $participants, $doctrine, $systemSettingsManager, 'App\Entity\Participant');

        $this->assertSame([], $facade->expiredParticipants());
    }

    public function testPseudonymizeParticipantsAnonymizesAttendancesButKeepsThem(): void
    {
        $connection = $this->createMock(Connection::class);
        // The concrete repository (not the interface) exposes createQueryBuilder().
        $participants = $this->createMock(ParticipantRepository::class);
        $doctrine = $this->createMock(EntityManagerInterface::class);
        $systemSettingsManager = $this->createMock(SystemSettingsManager::class);

        $participant = new BaseParticipant();
        $attendance = new Attendance(new Base(), $participant);
        $participant->addAttendance($attendance);

        // The participant record is removed with a bulk query, bypassing the
        // cascade: remove that would otherwise delete the attendance we keep.
        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('delete')->willReturn($qb);
        $qb->method('where')->willReturn($qb);
        $qb->method('setParameter')->willReturn($qb);
        $query = $this->getMockBuilder(Query::class)->disableOriginalConstructor()->getMock();
        $qb->method('getQuery')->willReturn($query);
        $query->expects($this->once())->method('execute');
        $participants->method('createQueryBuilder')->willReturn($qb);
        $doctrine->expects($this->once())->method('flush');

        $facade = new EraseDataFacade($connection, $participants, $doctrine, $systemSettingsManager, 'App\Entity\Participant');
        $facade->pseudonymizeParticipants([$participant]);

        // The attendance survives, now anonymized: no participant, but a pseudonym
        // (so paid receipts keep their offer + fee line, only the name goes).
        self::assertNull($attendance->getParticipant());
        self::assertNotSame('', (string) $attendance->getParticipantPseudonym());
    }

    public function testPseudonymizeAccountAnonymizesChildrenAttendancesAndRemovesTheAccount(): void
    {
        $connection = $this->createMock(Connection::class);
        $participants = $this->createMock(ParticipantRepositoryInterface::class);
        $doctrine = $this->createMock(EntityManagerInterface::class);
        $systemSettingsManager = $this->createMock(SystemSettingsManager::class);

        $account = new User();
        $participant = new BaseParticipant();
        $account->addParticipant($participant);
        $attendance = new Attendance(new Base(), $participant);
        $participant->addAttendance($attendance);

        $activityRepo = $this->createMock(EntityRepository::class);
        $activityRepo->method('findBy')->willReturn([]);
        $doctrine->method('getRepository')->willReturn($activityRepo);
        // The account is removed through the ORM (so its own associations cascade);
        // the children come with it, the attendances do not.
        $doctrine->expects($this->once())->method('remove')->with($account);
        $doctrine->expects($this->once())->method('flush');

        $facade = new EraseDataFacade($connection, $participants, $doctrine, $systemSettingsManager, 'App\Entity\Participant');
        $facade->pseudonymizeAccount($account);

        // The attendance is detached from the participant (so the cascade keeps
        // it) and anonymized — the receipt keeps its offer + fee, only the name goes.
        self::assertCount(0, $participant->getAttendances());
        self::assertNull($attendance->getParticipant());
        self::assertNotSame('', (string) $attendance->getParticipantPseudonym());
    }

    public function testPseudonymizeParticipantsIsNoOpForEmptyList(): void
    {
        $connection = $this->createMock(Connection::class);
        $participants = $this->createMock(ParticipantRepository::class);
        $doctrine = $this->createMock(EntityManagerInterface::class);
        $systemSettingsManager = $this->createMock(SystemSettingsManager::class);

        $participants->expects($this->never())->method('createQueryBuilder');
        $doctrine->expects($this->never())->method('flush');

        $facade = new EraseDataFacade($connection, $participants, $doctrine, $systemSettingsManager, 'App\Entity\Participant');
        $facade->pseudonymizeParticipants([]);
    }
}
