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

use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Comment\BrevoAttachmentDownloader;
use Ferienpass\CoreBundle\Comment\InboundAttachmentIngest;
use Ferienpass\CoreBundle\Comment\InboundAttachmentPolicy;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\Validator\Validation;

/**
 * The storage layout of the shared attachments bucket. Both domains own a named
 * top-level prefix — `conversation/` here, `application/` for the file answers of
 * the application form. That is not cosmetic: S3 lifecycle rules and bucket
 * policies match by prefix and have no exclude operator, so a domain living at the
 * bucket root cannot be addressed by one at all.
 */
class InboundAttachmentIngestTest extends TestCase
{
    public function testAttachmentsAreStoredUnderTheConversationPrefix(): void
    {
        $item = new ConversationItem(new Base(), null, 'Anbei der Nachweis.');

        $storage = $this->createMock(VirtualFilesystemInterface::class);
        $storage->expects(self::once())->method('write')->with(
            self::matchesRegularExpression(\sprintf('#^conversation/%s/[0-9a-f]{16}\.png$#', preg_quote($item->getUuid()->toBase58(), '#'))),
            self::isString(),
        );

        $result = $this->ingest($storage)->ingest($item, [
            ['token' => 'tok', 'name' => 'nachweis.png', 'size' => 68],
        ]);

        self::assertSame(['stored' => 1, 'blocked' => 0], $result);
    }

    public function testAFileThePolicyRejectsIsNeverStored(): void
    {
        $item = new ConversationItem(new Base(), null, 'Anbei.');

        $storage = $this->createMock(VirtualFilesystemInterface::class);
        $storage->expects(self::never())->method('write');

        // A PNG announcing itself as an executable: the extension is not on the
        // allow-list, so nothing reaches the bucket.
        $result = $this->ingest($storage)->ingest($item, [
            ['token' => 'tok', 'name' => 'rechnung.exe', 'size' => 68],
        ]);

        self::assertSame(['stored' => 0, 'blocked' => 1], $result);
    }

    private function ingest(VirtualFilesystemInterface $storage): InboundAttachmentIngest
    {
        // The downloader is final, so it is wired to a mock HTTP client rather than
        // doubled — which also keeps its own streaming/size handling in the picture.
        $downloader = new BrevoAttachmentDownloader(
            new MockHttpClient(new MockResponse(self::png())),
            new NullLogger(),
        );

        return new InboundAttachmentIngest(
            $downloader,
            new InboundAttachmentPolicy(Validation::createValidator()),
            $storage,
            $this->createStub(EntityManagerInterface::class),
            new NullLogger(),
        );
    }

    private static function png(): string
    {
        return base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==', true);
    }
}
