<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Tests\EventListener;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\EventListener\EmailLogSubscriber;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Mailer\EmailLogMetadata;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Mailer\EmailLogStamp;
use CpCreation\VitiCore\Mailer\Model\EmailLog;
use CpCreation\VitiCore\Mailer\Repository\EmailLogRepository;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
use Doctrine\Persistence\ManagerRegistry;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Doctrine\UuidType;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Mailer\Envelope as MailerEnvelope;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\Event\SentMessageEvent;
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Messenger\Envelope as MessengerEnvelope;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
use Symfony\Component\Mime\Email;

final class EmailLogSubscriberTest extends TestCase
{
    private EntityManager $entityManager;
    private EmailLogRepository $emailLogs;
    private EmailLogSubscriber $subscriber;

    protected function setUp(): void
    {
        if (!Type::hasType('uuid')) {
            Type::addType('uuid', UuidType::class);
        }

        $configuration = ORMSetup::createAttributeMetadataConfiguration([
            \dirname(__DIR__, 2) . '/src/Mailer/Model',
        ], true);
        $connection = DriverManager::getConnection([
            'driver' => 'pdo_sqlite',
            'memory' => true,
        ], $configuration);
        $this->entityManager = new EntityManager($connection, $configuration);
        $connection->executeStatement(<<<'SQL'
            CREATE TABLE mailer_email_log (
                id VARCHAR(36) NOT NULL,
                type VARCHAR(80) NOT NULL,
                recipientEmail VARCHAR(255) NOT NULL,
                recipientName VARCHAR(255) DEFAULT NULL,
                subject VARCHAR(255) DEFAULT NULL,
                status VARCHAR(20) NOT NULL,
                transport VARCHAR(255) DEFAULT NULL,
                error CLOB DEFAULT NULL,
                queuedAt DATETIME NOT NULL,
                sentAt DATETIME DEFAULT NULL,
                failedAt DATETIME DEFAULT NULL,
                user_id VARCHAR(36) DEFAULT NULL,
                cart_id VARCHAR(36) DEFAULT NULL,
                PRIMARY KEY (id)
            )
            SQL);

        $registry = $this->createMock(ManagerRegistry::class);
        $registry->method('getManagerForClass')
            ->with(EmailLog::class)
            ->willReturn($this->entityManager);

        $this->emailLogs = new EmailLogRepository($registry);
        $this->subscriber = new EmailLogSubscriber($this->emailLogs, $this->entityManager);
    }

    public function testAsyncMailerLifecycleCreatesOnlyOneLog(): void
    {
        $original = $this->email();
        EmailLogMetadata::attach($original, 'review_invitation');

        // Mailer::send() publie l’événement queued sur une copie.
        $queuedMessage = clone $original;
        $queuedEvent = $this->messageEvent($queuedMessage, true);
        $this->subscriber->onMailerMessage($queuedEvent);
        $stamp = $queuedEvent->getStamps()[0] ?? null;

        self::assertInstanceOf(EmailLogStamp::class, $stamp);
        self::assertCount(1, $this->emailLogs->findAll());

        // Le transport du worker republie un MessageEvent non-queued depuis
        // le message original, qui contient encore le header de métadonnées.
        $workerMessage = clone $original;
        $this->subscriber->onMailerMessage($this->messageEvent($workerMessage, false));
        $this->subscriber->onMailerMessageSent(new SentMessageEvent(new SentMessage(
            $workerMessage,
            MailerEnvelope::create($workerMessage),
        )));

        self::assertFalse($workerMessage->getHeaders()->has(EmailLogMetadata::HEADER));
        self::assertCount(1, $this->emailLogs->findAll());
        self::assertSame(EmailLog::STATUS_QUEUED, $this->emailLogs->findAll()[0]->getStatus());

        $this->subscriber->onWorkerMessageHandled(new WorkerMessageHandledEvent(
            new MessengerEnvelope(new SendEmailMessage($original), [$stamp]),
            'async_mail',
        ));

        self::assertCount(1, $logs = $this->emailLogs->findAll());
        self::assertSame(EmailLog::STATUS_SENT, $logs[0]->getStatus());
    }

    public function testDirectTransportSuccessIsCorrelatedWithItsSentEvent(): void
    {
        $message = $this->email();
        EmailLogMetadata::attachForDirectTransport($message, 'review_invitation');
        $dispatcher = new EventDispatcher();
        $dispatcher->addSubscriber($this->subscriber);
        $transport = new class ($dispatcher) extends AbstractTransport {
            public ?SentMessage $deliveredMessage = null;

            protected function doSend(SentMessage $message): void
            {
                $this->deliveredMessage = $message;
            }

            public function __toString(): string
            {
                return 'smtp://mailer.example';
            }
        };

        $transport->send($message);

        self::assertCount(1, $logs = $this->emailLogs->findAll());
        self::assertSame(EmailLog::STATUS_SENT, $logs[0]->getStatus());
        self::assertNull($logs[0]->getError());
        self::assertInstanceOf(Email::class, $delivered = $transport->deliveredMessage?->getOriginalMessage());
        self::assertFalse($delivered->getHeaders()->has(EmailLogMetadata::HEADER));
        self::assertFalse($delivered->getHeaders()->has(EmailLogMetadata::DIRECT_TRANSPORT_HEADER));
    }

    public function testDirectTransportFailureIsCorrelatedWithItsFailedEvent(): void
    {
        $message = $this->email();
        EmailLogMetadata::attachForDirectTransport($message, 'review_invitation_reminder');
        $dispatcher = new EventDispatcher();
        $dispatcher->addSubscriber($this->subscriber);
        $transport = new class ($dispatcher) extends AbstractTransport {
            protected function doSend(SentMessage $message): void
            {
                throw new \RuntimeException('SMTP indisponible');
            }

            public function __toString(): string
            {
                return 'smtp://mailer.example';
            }
        };

        try {
            $transport->send($message);
            self::fail('Le transport aurait dû propager l’échec SMTP.');
        } catch (\RuntimeException $error) {
            self::assertSame('SMTP indisponible', $error->getMessage());
        }

        self::assertCount(1, $logs = $this->emailLogs->findAll());
        self::assertSame(EmailLog::STATUS_FAILED, $logs[0]->getStatus());
        self::assertSame('SMTP indisponible', $logs[0]->getError());
    }

    private function email(): Email
    {
        return new Email()
            ->from('boutique@example.com')
            ->to('client@example.com')
            ->subject('Votre avis')
            ->text('Donnez votre avis.');
    }

    private function messageEvent(Email $message, bool $queued): MessageEvent
    {
        return new MessageEvent(
            $message,
            MailerEnvelope::create($message),
            'smtp://mailer.example',
            $queued,
        );
    }
}
