<?php

declare(strict_types=1);

namespace App\Tests\Shared\Infrastructure\Doctrine;

use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
use App\Shared\Infrastructure\Doctrine\TimestampableBlamableSubscriber;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Tests unitaires du TimestampableBlamableSubscriber.
 *
 * On exerce directement prePersist / preUpdate avec un EntityManager et une
 * Security stubbes — aucun boot de kernel, aucun acces BDD. Les entites de test
 * sont des fixtures internes (cf. bas de fichier).
 *
 * @internal
 */
final class TimestampableBlamableSubscriberTest extends TestCase
{
    public function testPrePersistWithUser(): void
    {
        $user       = $this->createStub(UserInterface::class);
        $subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user), new MockClock());
        $entity     = new FullAuditableFixture();

        $subscriber->prePersist($this->prePersistArgs($entity));

        // Les 4 colonnes sont remplies : dates posees, blame = user courant.
        self::assertInstanceOf(DateTimeImmutable::class, $entity->getCreatedAt());
        self::assertInstanceOf(DateTimeImmutable::class, $entity->getUpdatedAt());
        self::assertSame($entity->getCreatedAt(), $entity->getUpdatedAt());
        self::assertSame($user, $entity->getCreatedBy());
        self::assertSame($user, $entity->getUpdatedBy());
    }

    public function testPrePersistWithoutUser(): void
    {
        $subscriber = new TimestampableBlamableSubscriber($this->securityReturning(null), new MockClock());
        $entity     = new FullAuditableFixture();

        $subscriber->prePersist($this->prePersistArgs($entity));

        // Hors contexte HTTP (CLI / cron) : dates remplies, blame laisse a null.
        self::assertInstanceOf(DateTimeImmutable::class, $entity->getCreatedAt());
        self::assertInstanceOf(DateTimeImmutable::class, $entity->getUpdatedAt());
        self::assertNull($entity->getCreatedBy());
        self::assertNull($entity->getUpdatedBy());
    }

    public function testPreUpdate(): void
    {
        $user = $this->createStub(UserInterface::class);
        // Horloge figee 1s apres le createdAt simule : updatedAt doit avancer
        // de facon deterministe, sans dependre de l'heure reelle.
        $subscriber = new TimestampableBlamableSubscriber(
            $this->securityReturning($user),
            new MockClock(new DateTimeImmutable('2020-01-01 10:00:01')),
        );

        // On simule une entite deja persistee : createdAt fige dans le passe,
        // createdBy positionne par une creation anterieure.
        $createdAt = new DateTimeImmutable('2020-01-01 10:00:00');
        $entity    = new FullAuditableFixture();
        $entity->setCreatedAt($createdAt);
        $entity->setUpdatedAt($createdAt);

        $subscriber->preUpdate($this->preUpdateArgs($entity));

        // updatedAt avance, createdAt reste fige, updatedBy = user courant.
        self::assertSame($createdAt, $entity->getCreatedAt());
        self::assertGreaterThan($createdAt, $entity->getUpdatedAt());
        self::assertSame($user, $entity->getUpdatedBy());
    }

    public function testPartialEntityTimestampableOnly(): void
    {
        $user       = $this->createStub(UserInterface::class);
        $subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user), new MockClock());
        $entity     = new TimestampableOnlyFixture();

        // Entite Timestampable mais NON Blamable : seules les dates sont posees,
        // aucun appel de blame (et aucune erreur).
        $subscriber->prePersist($this->prePersistArgs($entity));

        self::assertInstanceOf(DateTimeImmutable::class, $entity->getCreatedAt());
        self::assertInstanceOf(DateTimeImmutable::class, $entity->getUpdatedAt());
    }

    /**
     * Security stubbee renvoyant l'utilisateur fourni (ou null).
     */
    private function securityReturning(?UserInterface $user): Security
    {
        $security = $this->createStub(Security::class);
        $security->method('getUser')->willReturn($user);

        return $security;
    }

    private function prePersistArgs(object $entity): PrePersistEventArgs
    {
        return new PrePersistEventArgs($entity, $this->createStub(EntityManagerInterface::class));
    }

    private function preUpdateArgs(object $entity): PreUpdateEventArgs
    {
        $changeSet = [];

        return new PreUpdateEventArgs($entity, $this->createStub(EntityManagerInterface::class), $changeSet);
    }
}

/**
 * Fixture interne : entite metier complete (Timestampable + Blamable) via le
 * Trait reel teste.
 *
 * @internal
 */
final class FullAuditableFixture implements TimestampableInterface, BlamableInterface
{
    use TimestampableBlamableTrait;
}

/**
 * Fixture interne : entite Timestampable seule (sans Blamable), pour verifier
 * la dissociation des deux contrats par le Subscriber.
 *
 * @internal
 */
final class TimestampableOnlyFixture implements TimestampableInterface
{
    private ?DateTimeImmutable $createdAt = null;
    private ?DateTimeImmutable $updatedAt = null;

    public function getCreatedAt(): ?DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function setCreatedAt(DateTimeImmutable $createdAt): void
    {
        $this->createdAt = $createdAt;
    }

    public function getUpdatedAt(): ?DateTimeImmutable
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(DateTimeImmutable $updatedAt): void
    {
        $this->updatedAt = $updatedAt;
    }
}
