<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Tests\User;

use CpCreation\VitiCore\User\ExpiredRegistrationPurger;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\TestCase;

final class ExpiredRegistrationPurgerTest extends TestCase
{
    public function testItIsIdempotentWhenNoRegistrationHasExpired(): void
    {
        $connection = $this->createMock(Connection::class);
        $connection
            ->expects(self::once())
            ->method('transactional')
            ->willReturnCallback(static fn (callable $callback): mixed => $callback($connection))
        ;
        $connection
            ->expects(self::once())
            ->method('fetchAllAssociative')
            ->willReturn([])
        ;
        $connection
            ->expects(self::never())
            ->method('executeStatement')
        ;

        $result = new ExpiredRegistrationPurger($connection)->purge(
            new \DateTimeImmutable('2026-08-27 12:00:00'),
        );

        self::assertSame(0, $result->eligibleCount);
        self::assertSame(0, $result->deletedCount);
        self::assertSame(0, $result->emailLogsDeleted);
        self::assertSame(0, $result->auditRowsDeleted);
        self::assertSame(0, $result->batchCount);
        self::assertFalse($result->hasMore);
    }

    public function testItDeletesOnlyStillEligibleRegistrationsAndTheirTechnicalTraces(): void
    {
        $firstId = '00000000-0000-0000-0000-000000000001';
        $secondId = '00000000-0000-0000-0000-000000000002';
        $fetchCall = 0;
        $connection = $this->createMock(Connection::class);
        $connection
            ->expects(self::once())
            ->method('transactional')
            ->willReturnCallback(static fn (callable $callback): mixed => $callback($connection))
        ;
        $connection
            ->expects(self::exactly(2))
            ->method('fetchAllAssociative')
            ->willReturnCallback(static function (string $sql, array $parameters) use ($firstId, $secondId, &$fetchCall): array {
                ++$fetchCall;

                if (2 === $fetchCall) {
                    self::assertStringContainsString('FROM mailer_email_log', $sql);
                    self::assertSame([$firstId, $secondId], $parameters['candidateIds']);

                    return [['id' => '00000000-0000-0000-0000-000000000010', 'user_id' => $firstId]];
                }

                self::assertStringContainsString('registration.is_verified = FALSE', $sql);
                self::assertStringContainsString('registration.email_verification_token IS NOT NULL', $sql);
                self::assertStringContainsString('registration.email_verification_expires_at <= :purgeBefore', $sql);
                self::assertStringContainsString('registration.updated_at <= :purgeBefore', $sql);
                self::assertEquals(
                    new \DateTimeImmutable('2026-08-20 12:00:00'),
                    $parameters['purgeBefore'],
                );
                self::assertStringContainsString('registration.last_login_at IS NULL', $sql);
                self::assertStringContainsString('registration.invoice_address_id IS NULL', $sql);
                self::assertStringContainsString('registration.delivery_address_id IS NULL', $sql);
                self::assertStringContainsString('registration.group_id IS NULL', $sql);
                self::assertStringContainsString('registration.roles::jsonb', $sql);
                self::assertStringContainsString('linked_cart.user_id = registration.id', $sql);
                self::assertStringContainsString('assigned_promo.assigned_user_id = registration.id', $sql);
                self::assertStringContainsString('business_log.type NOT IN (:technicalEmailTypes)', $sql);
                self::assertStringContainsString('guest_checkout.payed_at IS NOT NULL', $sql);
                self::assertStringContainsString('LOWER(BTRIM(guest_invoice.email))', $sql);
                self::assertStringContainsString('LOWER(BTRIM(guest_delivery.email))', $sql);
                self::assertStringContainsString('FOR UPDATE OF registration SKIP LOCKED', $sql);

                return [
                    ['id' => $firstId, 'email' => 'first@example.test'],
                    ['id' => $secondId, 'email' => 'second@example.test'],
                ];
            })
        ;
        $connection
            ->expects(self::once())
            ->method('fetchFirstColumn')
            ->willReturn([$firstId])
        ;
        $connection
            ->expects(self::exactly(2))
            ->method('executeStatement')
            ->willReturnOnConsecutiveCalls(1, 2)
        ;

        $result = new ExpiredRegistrationPurger($connection)->purge(
            new \DateTimeImmutable('2026-08-27 12:00:00'),
            batchSize: 2,
            maxBatches: 3,
        );

        self::assertSame(2, $result->eligibleCount);
        self::assertSame(1, $result->deletedCount);
        self::assertSame(1, $result->skippedCount);
        self::assertSame(1, $result->emailLogsDeleted);
        self::assertSame(2, $result->auditRowsDeleted);
        self::assertSame(1, $result->batchCount);
        self::assertTrue($result->hasMore);
    }

    public function testItBoundsEachExecutionAndReportsRemainingRegistrations(): void
    {
        $firstId = '00000000-0000-0000-0000-000000000001';
        $secondId = '00000000-0000-0000-0000-000000000002';
        $connection = $this->createMock(Connection::class);
        $connection
            ->expects(self::once())
            ->method('transactional')
            ->willReturnCallback(static fn (callable $callback): mixed => $callback($connection))
        ;
        $connection
            ->expects(self::exactly(2))
            ->method('fetchAllAssociative')
            ->willReturnOnConsecutiveCalls(
                [
                    ['id' => $firstId, 'email' => 'first@example.test'],
                    ['id' => $secondId, 'email' => 'second@example.test'],
                ],
                [
                    ['id' => '00000000-0000-0000-0000-000000000010', 'user_id' => $firstId],
                    ['id' => '00000000-0000-0000-0000-000000000011', 'user_id' => $secondId],
                ],
            )
        ;
        $connection
            ->expects(self::once())
            ->method('fetchFirstColumn')
            ->willReturn([$firstId, $secondId])
        ;
        $connection
            ->expects(self::exactly(2))
            ->method('executeStatement')
            ->willReturnOnConsecutiveCalls(2, 2)
        ;
        $connection
            ->expects(self::once())
            ->method('fetchOne')
            ->willReturn(1)
        ;

        $result = new ExpiredRegistrationPurger($connection)->purge(
            new \DateTimeImmutable('2026-08-27 12:00:00'),
            batchSize: 2,
            maxBatches: 1,
        );

        self::assertSame(2, $result->eligibleCount);
        self::assertSame(2, $result->deletedCount);
        self::assertSame(0, $result->skippedCount);
        self::assertSame(1, $result->batchCount);
        self::assertTrue($result->hasMore);
    }
}
