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

use Doctrine\ORM\Mapping\ClassMetadata;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\MessengerLog;
use Ferienpass\CoreBundle\Entity\Offer;
use Ferienpass\CoreBundle\Fixtures\Doctrine\OrmTestCase;

/**
 * Offer::$messengerLogs used to be declared `mappedBy: 'attendances'`, which names
 * MessengerLog's association to Attendance. The queries built on it then joined
 * AttendanceMessengerLog and matched offer ids against attendance ids, so an offer
 * could never see its own log rows — silently, and for every consumer at once.
 */
class OfferMessengerLogsTest extends OrmTestCase
{
    public function testAssociationIsTheInverseSideOfMessengerLogOffers(): void
    {
        $mapping = $this->em->getClassMetadata(Offer::class)->getAssociationMapping('messengerLogs');

        self::assertSame(MessengerLog::class, $mapping->targetEntity);
        self::assertSame('offers', $mapping->mappedBy);
        self::assertFalse($mapping->isOwningSide());

        // The owning side must point back, or the pair is only half declared.
        $owning = $this->em->getClassMetadata(MessengerLog::class)->getAssociationMapping('offers');

        self::assertSame('messengerLogs', $owning->inversedBy);
        self::assertSame('OfferMessengerLog', $owning->joinTable->name);
    }

    public function testJoinTraversesOfferMessengerLog(): void
    {
        $sql = $this->em
            ->createQuery(\sprintf('SELECT o.id, m.id FROM %s o INNER JOIN o.messengerLogs m', Offer::class))
            ->getSQL()
        ;

        self::assertStringContainsString('OfferMessengerLog', $sql);
        self::assertMatchesRegularExpression('/JOIN OfferMessengerLog \w+ ON \w+\.id = \w+\.offer_id/', $sql);
        self::assertStringNotContainsString('AttendanceMessengerLog', $sql);
    }

    public function testCollectionReadsBackTheOffersOwnLogRows(): void
    {
        $offer = $this->createOffer();
        $other = $this->createOffer();

        $this->em->persist($log = new MessengerLog('Some\Message', [$offer]));
        $this->em->flush();
        $this->em->clear();

        $reloaded = $this->em->find(Offer::class, $offer->getId());
        self::assertCount(1, $reloaded->getMessengerLogs());
        self::assertSame($log->getId(), $reloaded->getMessengerLogs()->first()->getId());

        self::assertCount(0, $this->em->find(Offer::class, $other->getId())->getMessengerLogs());
    }

    /**
     * The attendance pair is the one this association was mistakenly borrowing; it is a
     * separate join table and must stay separate.
     */
    public function testAttendanceLogsDoNotLeakIntoTheOfferCollection(): void
    {
        $offer = $this->createOffer();

        $attendance = new Attendance($offer, null);
        $attendance->setModifiedAt(new \DateTimeImmutable());
        $this->em->persist($attendance);
        $this->em->flush();

        $this->em->persist(new MessengerLog('Some\Message', [$attendance]));
        $this->em->flush();
        $this->em->clear();

        self::assertCount(1, $this->em->find(Attendance::class, $attendance->getId())->getMessengerLogs());
        self::assertCount(0, $this->em->find(Offer::class, $offer->getId())->getMessengerLogs());
    }

    public function testJoinTableIsUnchangedBySchema(): void
    {
        $metadata = $this->em->getClassMetadata(MessengerLog::class);
        \assert($metadata instanceof ClassMetadata);

        $joinTable = $metadata->getAssociationMapping('offers')->joinTable;

        self::assertSame('log_id', $joinTable->joinColumns[0]->name);
        self::assertSame('offer_id', $joinTable->inverseJoinColumns[0]->name);
    }
}
