<?php

declare(strict_types=1);

namespace ShlinkioTest\Shlink\Core\EventDispatcher;

use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToMercure;
use Shlinkio\Shlink\Core\Mercure\MercureUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\Model\Visitor;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;

class NotifyVisitToMercureTest extends TestCase
{
    use ProphecyTrait;

    private NotifyVisitToMercure $listener;
    private ObjectProphecy $hub;
    private ObjectProphecy $updatesGenerator;
    private ObjectProphecy $em;
    private ObjectProphecy $logger;

    public function setUp(): void
    {
        $this->hub = $this->prophesize(HubInterface::class);
        $this->updatesGenerator = $this->prophesize(MercureUpdatesGeneratorInterface::class);
        $this->em = $this->prophesize(EntityManagerInterface::class);
        $this->logger = $this->prophesize(LoggerInterface::class);

        $this->listener = new NotifyVisitToMercure(
            $this->hub->reveal(),
            $this->updatesGenerator->reveal(),
            $this->em->reveal(),
            $this->logger->reveal(),
        );
    }

    /** @test */
    public function notificationsAreNotSentWhenVisitCannotBeFound(): void
    {
        $visitId = '123';
        $findVisit = $this->em->find(Visit::class, $visitId)->willReturn(null);
        $logWarning = $this->logger->warning(
            'Tried to notify mercure for visit with id "{visitId}", but it does not exist.',
            ['visitId' => $visitId],
        );
        $logDebug = $this->logger->debug(Argument::cetera());
        $buildNewShortUrlVisitUpdate = $this->updatesGenerator->newShortUrlVisitUpdate(
            Argument::type(Visit::class),
        );
        $buildNewOrphanVisitUpdate = $this->updatesGenerator->newOrphanVisitUpdate(Argument::type(Visit::class));
        $buildNewVisitUpdate = $this->updatesGenerator->newVisitUpdate(Argument::type(Visit::class));
        $publish = $this->hub->publish(Argument::type(Update::class));

        ($this->listener)(new VisitLocated($visitId));

        $findVisit->shouldHaveBeenCalledOnce();
        $logWarning->shouldHaveBeenCalledOnce();
        $logDebug->shouldNotHaveBeenCalled();
        $buildNewShortUrlVisitUpdate->shouldNotHaveBeenCalled();
        $buildNewVisitUpdate->shouldNotHaveBeenCalled();
        $buildNewOrphanVisitUpdate->shouldNotHaveBeenCalled();
        $publish->shouldNotHaveBeenCalled();
    }

    /** @test */
    public function notificationsAreSentWhenVisitIsFound(): void
    {
        $visitId = '123';
        $visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance());
        $update = new Update('', '');

        $findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit);
        $logWarning = $this->logger->warning(Argument::cetera());
        $logDebug = $this->logger->debug(Argument::cetera());
        $buildNewShortUrlVisitUpdate = $this->updatesGenerator->newShortUrlVisitUpdate($visit)->willReturn($update);
        $buildNewOrphanVisitUpdate = $this->updatesGenerator->newOrphanVisitUpdate($visit)->willReturn($update);
        $buildNewVisitUpdate = $this->updatesGenerator->newVisitUpdate($visit)->willReturn($update);
        $publish = $this->hub->publish($update);

        ($this->listener)(new VisitLocated($visitId));

        $findVisit->shouldHaveBeenCalledOnce();
        $logWarning->shouldNotHaveBeenCalled();
        $logDebug->shouldNotHaveBeenCalled();
        $buildNewShortUrlVisitUpdate->shouldHaveBeenCalledOnce();
        $buildNewVisitUpdate->shouldHaveBeenCalledOnce();
        $buildNewOrphanVisitUpdate->shouldNotHaveBeenCalled();
        $publish->shouldHaveBeenCalledTimes(2);
    }

    /** @test */
    public function debugIsLoggedWhenExceptionIsThrown(): void
    {
        $visitId = '123';
        $visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance());
        $update = new Update('', '');
        $e = new RuntimeException('Error');

        $findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit);
        $logWarning = $this->logger->warning(Argument::cetera());
        $logDebug = $this->logger->debug('Error while trying to notify mercure hub with new visit. {e}', [
            'e' => $e,
        ]);
        $buildNewShortUrlVisitUpdate = $this->updatesGenerator->newShortUrlVisitUpdate($visit)->willReturn($update);
        $buildNewOrphanVisitUpdate = $this->updatesGenerator->newOrphanVisitUpdate($visit)->willReturn($update);
        $buildNewVisitUpdate = $this->updatesGenerator->newVisitUpdate($visit)->willReturn($update);
        $publish = $this->hub->publish($update)->willThrow($e);

        ($this->listener)(new VisitLocated($visitId));

        $findVisit->shouldHaveBeenCalledOnce();
        $logWarning->shouldNotHaveBeenCalled();
        $logDebug->shouldHaveBeenCalledOnce();
        $buildNewShortUrlVisitUpdate->shouldHaveBeenCalledOnce();
        $buildNewVisitUpdate->shouldHaveBeenCalledOnce();
        $buildNewOrphanVisitUpdate->shouldNotHaveBeenCalled();
        $publish->shouldHaveBeenCalledOnce();
    }

    /**
     * @test
     * @dataProvider provideOrphanVisits
     */
    public function notificationsAreSentForOrphanVisits(Visit $visit): void
    {
        $visitId = '123';
        $update = new Update('', '');

        $findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit);
        $logWarning = $this->logger->warning(Argument::cetera());
        $logDebug = $this->logger->debug(Argument::cetera());
        $buildNewShortUrlVisitUpdate = $this->updatesGenerator->newShortUrlVisitUpdate($visit)->willReturn($update);
        $buildNewOrphanVisitUpdate = $this->updatesGenerator->newOrphanVisitUpdate($visit)->willReturn($update);
        $buildNewVisitUpdate = $this->updatesGenerator->newVisitUpdate($visit)->willReturn($update);
        $publish = $this->hub->publish($update);

        ($this->listener)(new VisitLocated($visitId));

        $findVisit->shouldHaveBeenCalledOnce();
        $logWarning->shouldNotHaveBeenCalled();
        $logDebug->shouldNotHaveBeenCalled();
        $buildNewShortUrlVisitUpdate->shouldNotHaveBeenCalled();
        $buildNewVisitUpdate->shouldNotHaveBeenCalled();
        $buildNewOrphanVisitUpdate->shouldHaveBeenCalledOnce();
        $publish->shouldHaveBeenCalledOnce();
    }

    public function provideOrphanVisits(): iterable
    {
        $visitor = Visitor::emptyInstance();

        yield Visit::TYPE_REGULAR_404 => [Visit::forRegularNotFound($visitor)];
        yield Visit::TYPE_INVALID_SHORT_URL => [Visit::forInvalidShortUrl($visitor)];
        yield Visit::TYPE_BASE_URL => [Visit::forBasePath($visitor)];
    }
}