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

use Contao\CoreBundle\Cron\Cron;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Ferienpass\CoreBundle\Cron\TruncateUnactivatedUsers;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\UserRepository;
use PHPUnit\Framework\TestCase;

final class TruncateUnactivatedUsersTest extends TestCase
{
    public function testDeletesOnlyAccountsThatWereNeverActivated(): void
    {
        $dql = $this->captureDeleteDql();

        self::assertStringContainsString('user.disable = 1', $dql);
        self::assertStringContainsString('user.activatedAt IS NULL', $dql);
        self::assertStringContainsString('user.createdAt <= :graceCutoff', $dql);
    }

    public function testSparesAccountsThatShowAnySignOfHavingBeenReal(): void
    {
        // activatedAt cannot be backfilled for accounts activated without a log
        // (manual activation, or the notification disabled). These corroborating
        // guards keep the cron off any such row so it never reaps a live account.
        $dql = $this->captureDeleteDql();

        // A login is only possible on an activated account.
        self::assertStringContainsString('user.lastLogin IS NULL', $dql);

        // An explicit deletion hold, as the data-erase path honours.
        self::assertStringContainsString('user.dontDeleteBefore IS NULL', $dql);
        self::assertStringContainsString('user.dontDeleteBefore < CURRENT_TIMESTAMP()', $dql);

        // Only the recent window: a long-disabled, never-activated row predates
        // reliable activatedAt tracking rather than being a fresh drop-off.
        self::assertStringContainsString('user.createdAt >= :abandonWindowStart', $dql);
    }

    public function testDoesNotDependOnTheNotificationLog(): void
    {
        // The bug this replaced: an AccountActivated log entry only exists when
        // that mail was actually sent, so it is missing for every manually
        // activated account and on every instance where the notification is
        // disabled. Reading it as "was never activated" deleted live accounts.
        $dql = $this->captureDeleteDql();

        self::assertStringNotContainsString('MessengerLog', $dql);
        self::assertStringNotContainsString('AccountActivated', $dql);
    }

    private function captureDeleteDql(): string
    {
        $dql = null;

        $query = $this->createMock(Query::class);
        $query->method('setParameters')->willReturnSelf();
        $query->method('setFirstResult')->willReturnSelf();
        $query->method('setMaxResults')->willReturnSelf();
        $query->expects(self::once())->method('execute');

        $em = $this->createMock(EntityManagerInterface::class);
        $em->expects(self::once())
            ->method('createQuery')
            ->willReturnCallback(static function (string $capturedDql) use (&$dql, $query): Query {
                $dql = $capturedDql;

                return $query;
            })
        ;

        $users = $this->createStub(UserRepository::class);
        $users->method('createQueryBuilder')->willReturnCallback(
            static fn (string $alias): QueryBuilder => new QueryBuilder($em)->select($alias)->from(User::class, $alias),
        );

        (new TruncateUnactivatedUsers($users))(Cron::SCOPE_CLI);

        self::assertIsString($dql);

        return $dql;
    }
}
