<?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\Twig\Extension;

use Contao\CoreBundle\Filesystem\FilesystemItem;
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Contao\CoreBundle\Image\ImageFactoryInterface;
use Contao\Image\ImageInterface;
use Contao\Image\ImportantPart;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentState;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\DbafsLogo;
use Ferienpass\CoreBundle\Entity\DbafsMedia;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\OfferMedia;
use Ferienpass\CoreBundle\Entity\PageConfig;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Service\OfferPrivateLinkService;
use Ferienpass\CoreBundle\Settings\PageConfigManager;
use Ferienpass\CoreBundle\Twig\Extension\FerienpassTwigExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Uid\Uuid;

final class FerienpassTwigExtensionTest extends TestCase
{
    private FerienpassTwigExtension $extension;

    protected function setUp(): void
    {
        $this->extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );
    }

    public function testGenerateSignedUrlCallsUrlGeneratorAndSigner(): void
    {
        $urlGenerator = $this->createMock(UrlGeneratorInterface::class);
        $urlGenerator->method('generate')
            ->with('my_route', ['id' => 5])
            ->willReturn('/my-route?id=5');

        $signer = $this->createMock(UriSigner::class);
        $signer->method('sign')
            ->with('/my-route?id=5')
            ->willReturn('/my-route?id=5&_hash=abc123');

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(VirtualFilesystemInterface::class),
            $urlGenerator,
            $signer,
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        $result = $extension->generateSignedUrl('my_route', ['id' => 5]);
        self::assertSame('/my-route?id=5&_hash=abc123', $result);
    }

    public function testGenerateSignedUrlWithNoParameters(): void
    {
        $urlGenerator = $this->createMock(UrlGeneratorInterface::class);
        $urlGenerator->method('generate')
            ->with('home', [])
            ->willReturn('/');

        $signer = $this->createMock(UriSigner::class);
        $signer->method('sign')->with('/')->willReturn('/?_hash=xyz');

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(VirtualFilesystemInterface::class),
            $urlGenerator,
            $signer,
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        $result = $extension->generateSignedUrl('home');
        self::assertSame('/?_hash=xyz', $result);
    }

    public function testGetPageConfigDelegatesToManager(): void
    {
        $pageConfig = new PageConfig();

        $manager = $this->createMock(PageConfigManager::class);
        $manager->method('getPageConfig')->willReturn($pageConfig);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $manager,
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        self::assertSame($pageConfig, $extension->getPageConfig());
    }

    public function testToFilesystemItemWithDbafsLogo(): void
    {
        $uuid = Uuid::v7();
        $logo = $this->createStub(DbafsLogo::class);
        $logo->method('getUuid')->willReturn($uuid);

        $item = $this->createStub(FilesystemItem::class);

        $logosStorage = $this->createMock(VirtualFilesystemInterface::class);
        $logosStorage->expects(self::once())->method('get')->with($uuid)->willReturn($item);

        $extension = new FerienpassTwigExtension(
            $logosStorage,
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        self::assertSame($item, $extension->toFilesystemItem($logo));
    }

    public function testToFilesystemItemWithDbafsMedia(): void
    {
        $uuid = Uuid::v7();
        $media = $this->createStub(DbafsMedia::class);
        $media->method('getUuid')->willReturn($uuid);

        $item = $this->createStub(FilesystemItem::class);

        $mediaStorage = $this->createMock(VirtualFilesystemInterface::class);
        $mediaStorage->expects(self::once())->method('get')->with($uuid)->willReturn($item);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $mediaStorage,
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        self::assertSame($item, $extension->toFilesystemItem($media));
    }

    public function testToFilesystemItemWithOfferMedia(): void
    {
        $uuid = Uuid::v7();
        $dbafsMedia = $this->createStub(DbafsMedia::class);
        $dbafsMedia->method('getUuid')->willReturn($uuid);

        $offerMedia = $this->createStub(OfferMedia::class);
        $offerMedia->method('getMedia')->willReturn($dbafsMedia);

        $item = $this->createStub(FilesystemItem::class);

        $mediaStorage = $this->createMock(VirtualFilesystemInterface::class);
        $mediaStorage->expects(self::once())->method('get')->with($uuid)->willReturn($item);

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

        $extension = new FerienpassTwigExtension(
            $logosStorage,
            $mediaStorage,
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        self::assertSame($item, $extension->toFilesystemItem($offerMedia));
    }

    public function testToImageReturnsNullWhenUuidUnresolved(): void
    {
        $uuid = Uuid::v7();
        $media = $this->createStub(DbafsMedia::class);
        $media->method('getUuid')->willReturn($uuid);

        $mediaStorage = $this->createMock(VirtualFilesystemInterface::class);
        $mediaStorage->expects(self::once())->method('resolveUuid')->with($uuid)->willThrowException(new \RuntimeException('unresolved'));

        $imageFactory = $this->createMock(ImageFactoryInterface::class);
        $imageFactory->expects(self::never())->method('create');

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $mediaStorage,
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $imageFactory,
            '/tmp/project',
        );

        self::assertNull($extension->toImage($media));
    }

    public function testToImageAppliesImportantPartFromMedia(): void
    {
        $uuid = Uuid::v7();
        $media = $this->createStub(DbafsMedia::class);
        $media->method('getUuid')->willReturn($uuid);
        $media->method('getImportantPart')->willReturn([
            'x' => 0.1,
            'y' => 0.2,
            'width' => 0.3,
            'height' => 0.4,
        ]);

        $image = $this->createMock(ImageInterface::class);
        $image->expects(self::once())
            ->method('setImportantPart')
            ->with(self::callback(static function (ImportantPart $part): bool {
                return abs($part->getX() - 0.1) < 1e-9
                    && abs($part->getY() - 0.2) < 1e-9
                    && abs($part->getWidth() - 0.3) < 1e-9
                    && abs($part->getHeight() - 0.4) < 1e-9;
            }));

        $mediaStorage = $this->createMock(VirtualFilesystemInterface::class);
        $mediaStorage->method('resolveUuid')->with($uuid)->willReturn('ab/cd/file.jpg');

        $imageFactory = $this->createMock(ImageFactoryInterface::class);
        $imageFactory->expects(self::once())
            ->method('create')
            ->with('/tmp/project/storage/media/ab/cd/file.jpg')
            ->willReturn($image);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $mediaStorage,
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $imageFactory,
            '/tmp/project',
        );

        self::assertSame($image, $extension->toImage($media));
    }

    public function testToImageSkipsImportantPartWhenZero(): void
    {
        $uuid = Uuid::v7();
        $media = $this->createStub(DbafsMedia::class);
        $media->method('getUuid')->willReturn($uuid);
        $media->method('getImportantPart')->willReturn([
            'x' => 0.0,
            'y' => 0.0,
            'width' => 0.0,
            'height' => 0.0,
        ]);

        $image = $this->createMock(ImageInterface::class);
        $image->expects(self::never())->method('setImportantPart');

        $mediaStorage = $this->createMock(VirtualFilesystemInterface::class);
        $mediaStorage->method('resolveUuid')->with($uuid)->willReturn('file.jpg');

        $imageFactory = $this->createMock(ImageFactoryInterface::class);
        $imageFactory->method('create')->willReturn($image);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $mediaStorage,
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $imageFactory,
            '/tmp/project',
        );

        self::assertSame($image, $extension->toImage($media));
    }

    public function testToImageUnwrapsOfferMedia(): void
    {
        $uuid = Uuid::v7();
        $dbafsMedia = $this->createStub(DbafsMedia::class);
        $dbafsMedia->method('getUuid')->willReturn($uuid);
        $dbafsMedia->method('getImportantPart')->willReturn([
            'x' => 0.0,
            'y' => 0.0,
            'width' => 0.0,
            'height' => 0.0,
        ]);

        $offerMedia = $this->createStub(OfferMedia::class);
        $offerMedia->method('getMedia')->willReturn($dbafsMedia);

        $image = $this->createMock(ImageInterface::class);

        $mediaStorage = $this->createMock(VirtualFilesystemInterface::class);
        $mediaStorage->expects(self::once())->method('resolveUuid')->with($uuid)->willReturn('file.jpg');

        $imageFactory = $this->createMock(ImageFactoryInterface::class);
        $imageFactory->expects(self::once())->method('create')->willReturn($image);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $mediaStorage,
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $imageFactory,
            '/tmp/project',
        );

        self::assertSame($image, $extension->toImage($offerMedia));
    }

    public function testGetConsentStatesWithAttendance(): void
    {
        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);

        $state = new ConsentState();
        $consentManager = $this->createMock(ConsentManager::class);
        $consentManager->expects(self::once())
            ->method('forOfferAndParticipants')
            ->with($offer, [$participant])
            ->willReturn([$state]);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $consentManager,
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        $result = $extension->getConsentStates($attendance);
        self::assertCount(1, $result);
        self::assertSame($state, $result[0]);
    }

    public function testGetConsentStatesWithOffer(): void
    {
        $offer = new Base();
        $participant = new BaseParticipant();

        $state = new ConsentState();
        $consentManager = $this->createMock(ConsentManager::class);
        $consentManager->expects(self::once())
            ->method('forOfferAndParticipants')
            ->with($offer, [$participant])
            ->willReturn([$state]);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(UrlGeneratorInterface::class),
            $this->createMock(UriSigner::class),
            $consentManager,
            $this->createMock(PageConfigManager::class),
            $this->createMock(OfferPrivateLinkService::class),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        $result = $extension->getConsentStates($offer, $participant);
        self::assertCount(1, $result);
    }

    public function testGetOfferPrivateLink(): void
    {
        $uuid = Uuid::v7();
        $offer = $this->createStub(OfferInterface::class);
        $offer->method('getUuid')->willReturn($uuid);

        $token = rtrim(strtr(base64_encode($uuid->toBinary()), '+/', '-_'), '=');

        $urlGenerator = $this->createMock(UrlGeneratorInterface::class);
        $urlGenerator->expects(self::once())
            ->method('generate')
            ->with('cms_offer_private_link', self::anything(), UrlGeneratorInterface::ABSOLUTE_URL)
            ->willReturn('https://example.com/p/'.$token);

        $extension = new FerienpassTwigExtension(
            $this->createMock(VirtualFilesystemInterface::class),
            $this->createMock(VirtualFilesystemInterface::class),
            $urlGenerator,
            $this->createMock(UriSigner::class),
            $this->createMock(ConsentManager::class),
            $this->createMock(PageConfigManager::class),
            new OfferPrivateLinkService(),
            $this->createMock(ImageFactoryInterface::class),
            '/tmp/project',
        );

        self::assertSame('https://example.com/p/'.$token, $extension->getOfferPrivateLink($offer));
    }
}
