<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Tests\Notification\Email;

use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PostPersistEventArgs;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Translation\Translator;
use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;

/**
 * @internal
 *
 * @coversNothing
 */
class NotificationMailerTest extends TestCase
{
    use ProphecyTrait;

    public function testPostPersistComment(): void
    {
        $user1 = (new User())->setEmail('user1@foo.com');
        $user2 = (new User())->setEmail('user2@foo.com');
        $user3 = (new User())->setEmail('user3@foo.com');

        $notification = new Notification();
        $notification
            ->setTitle('test notification')
            ->setSender($user1)
            ->addAddressee($user2)
            ->addAddressee($user3)
        ;

        $comment = (new NotificationComment())
            ->setContent('foo bar baz')
            ->setCreatedBy($user2)
        ;
        $notification->addComment($comment);

        $mailer = $this->prophesize(MailerInterface::class);

        // a mail only to user1 and user3 should have been sent
        $mailer->send(Argument::that(function (Email $email) {
            foreach ($email->getTo() as $address) {
                if ('user1@foo.com' === $address->getAddress()) {
                    return true;
                }
            }

            return false;
        }))->shouldBeCalledTimes(1);
        $mailer->send(Argument::that(function (Email $email) {
            foreach ($email->getTo() as $address) {
                if ('user3@foo.com' === $address->getAddress()) {
                    return true;
                }
            }

            return false;
        }))->shouldBeCalledTimes(1);

        $objectManager = $this->prophesize(EntityManagerInterface::class);

        $mailer = $this->buildNotificationMailer($mailer->reveal());
        $mailer->postPersistComment($comment, new PostPersistEventArgs($comment, $objectManager->reveal()));
    }

    public function testPostPersistCommentDestWithNullEmail(): void
    {
        $user1 = (new User())->setEmail('user1@foo.com');
        $user2 = (new User())->setEmail('user2@foo.com');
        $user3 = (new User())->setEmail(null);

        $notification = new Notification();
        $notification
            ->setTitle('test notification')
            ->setSender($user1)
            ->addAddressee($user2)
            ->addAddressee($user3)
        ;

        $comment = (new NotificationComment())
            ->setContent('foo bar baz')
            ->setCreatedBy($user2)
        ;
        $notification->addComment($comment);

        $mailer = $this->prophesize(MailerInterface::class);

        // a mail only to user1 and user3 should have been sent
        $mailer->send(Argument::that(function (Email $email) {
            foreach ($email->getTo() as $address) {
                if ('user1@foo.com' === $address->getAddress()) {
                    return true;
                }
            }

            return false;
        }))->shouldBeCalledTimes(1);

        $objectManager = $this->prophesize(EntityManagerInterface::class);

        $mailer = $this->buildNotificationMailer($mailer->reveal());
        $mailer->postPersistComment($comment, new PostPersistEventArgs($comment, $objectManager->reveal()));
    }

    /**
     * @throws \ReflectionException
     * @throws Exception
     */
    public function testPostPersistNotificationToGroup(): void
    {
        // Create a real notification entity
        $notification = new Notification();
        $notification->setType('test_notification_type');

        // Use reflection to set the ID since it's normally generated by the database
        $reflectionNotification = new \ReflectionClass(Notification::class);
        $idProperty = $reflectionNotification->getProperty('id');
        $idProperty->setValue($notification, 123);

        // Create a real user entity
        $user = new User();
        $user->setEmail('user@example.com');
        $userGroup = new UserGroup();
        $userGroup->addUser($user);
        $notification->addAddressee($userGroup);

        // Use reflection to set the ID since it's normally generated by the database
        $reflectionUser = new \ReflectionClass($user);
        $idProperty = $reflectionUser->getProperty('id');
        $idProperty->setValue($user, 456);

        $reflectionUser = new \ReflectionClass($userGroup);
        $idProperty = $reflectionUser->getProperty('id');
        $idProperty->setValue($userGroup, 789);

        // Set notification flags for the user
        $user->setNotificationImmediately('test_notification_type', true);

        $messageBus = $this->prophesize(MessageBusInterface::class);
        $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled();

        $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && null === $message->getUserId() && 789 === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled();

        $notificationMailer = $this->buildNotificationMailer(null, $messageBus->reveal());

        $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal()));
    }

    /**
     * @throws \ReflectionException
     * @throws Exception
     */
    public function testPostPersistNotificationWithImmediateEmailPreference(): void
    {
        // Create a real notification entity
        $notification = new Notification();
        $notification->setType('test_notification_type');

        // Use reflection to set the ID since it's normally generated by the database
        $reflectionNotification = new \ReflectionClass(Notification::class);
        $idProperty = $reflectionNotification->getProperty('id');
        $idProperty->setValue($notification, 123);

        // Create a real user entity
        $user = new User();
        $user->setEmail('user@example.com');
        $notification->addAddressee($user);

        // Use reflection to set the ID since it's normally generated by the database
        $reflectionUser = new \ReflectionClass(User::class);
        $idProperty = $reflectionUser->getProperty('id');
        $idProperty->setValue($user, 456);

        // Set notification flags for the user
        $user->setNotificationImmediately('test_notification_type', true);

        $messageBus = $this->prophesize(MessageBusInterface::class);
        $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled();

        $notificationMailer = $this->buildNotificationMailer(null, $messageBus->reveal());

        $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal()));
    }

    public function testPostPersistNotificationWithDailyDigestPreference(): void
    {
        // Create a real notification entity
        $notification = new Notification();
        $notification->setType('test_notification_type');

        // Use reflection to set the ID since it's normally generated by the database
        $reflectionNotification = new \ReflectionClass(Notification::class);
        $idProperty = $reflectionNotification->getProperty('id');
        $idProperty->setAccessible(true);
        $idProperty->setValue($notification, 123);

        // Create a real user entity
        $user = new User();
        $user->setEmail('user@example.com');
        // Set notification flags for the user
        $user->setNotificationImmediately('test_notification_type', false);
        $user->setNotificationDailyDigest('test_notification_type', true);

        $notification->addAddressee($user);

        // Use reflection to set the ID since it's normally generated by the database
        $reflectionUser = new \ReflectionClass(User::class);
        $idProperty = $reflectionUser->getProperty('id');
        $idProperty->setAccessible(true);
        $idProperty->setValue($user, 456);

        $messageBus = $this->prophesize(MessageBusInterface::class);
        $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldNotBeCalled();

        $notificationMailer = $this->buildNotificationMailer(
            null,
            $messageBus->reveal()
        );

        $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal()));
    }

    public function testSendDailyDigest(): void
    {
        // Create a user
        $user = new User();
        $user->setEmail('user@example.com');

        // Create some notifications
        $notification = $this->prophesize(Notification::class);
        $notification->getTitle()->willReturn('Notification 1');
        $notification->getMessage()->willReturn('Message 1');
        $notification->getId()->willReturn(123);

        $notification2 = $this->prophesize(Notification::class);
        $notification2->getTitle()->willReturn('Notification 2');
        $notification2->getMessage()->willReturn('Message 2');
        $notification2->getId()->willReturn(456);

        $notifications = [$notification, $notification2];

        // Mock the mailer to verify that an email is sent with the correct parameters
        $mailer = $this->prophesize(MailerInterface::class);
        $mailer->send(Argument::that(function ($email) use ($user) {
            // Verify that the email is sent to the correct user
            foreach ($email->getTo() as $address) {
                if ($user->getEmail() === $address->getAddress()) {
                    return true;
                }
            }

            return false;
        }))->shouldBeCalledOnce();

        // Create a translator that returns a fixed string for the subject
        $translator = $this->prophesize(TranslatorInterface::class);
        $translator->trans('notification.Daily Notification Digest')->willReturn('Daily Digest');

        // Create the notification mailer with the mocked mailer and translator
        $notificationMailer = $this->buildNotificationMailer($mailer->reveal(), null, $translator->reveal());

        // Call the sendDailyDigest method
        $notificationMailer->sendDailyDigest($user, $notifications);
    }

    public function testSendDailyDigestWithNoNotifications(): void
    {
        // Create a user
        $user = new User();
        $user->setEmail('user@example.com');

        // Empty notifications array
        $notifications = [];

        // Mock the mailer to verify that no email is sent
        $mailer = $this->prophesize(MailerInterface::class);
        $mailer->send(Argument::any())->shouldNotBeCalled();

        // Create the notification mailer with the mocked mailer
        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());

        // Call the sendDailyDigest method
        $notificationMailer->sendDailyDigest($user, $notifications);
    }

    public function testSendDailyDigestWithUserHavingNoEmail(): void
    {
        // Create a user with no email
        $user = new User();
        $user->setEmail(null);

        // Create some notifications
        $notification = $this->prophesize(Notification::class);
        $notification->getTitle()->willReturn('Notification 1');
        $notification->getMessage()->willReturn('Message 1');
        $notification->getId()->willReturn(123);

        $notifications = [$notification];

        // Mock the mailer to verify that no email is sent
        $mailer = $this->prophesize(MailerInterface::class);
        $mailer->send(Argument::any())->shouldNotBeCalled();

        // Create the notification mailer with the mocked mailer
        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());

        // Call the sendDailyDigest method
        $notificationMailer->sendDailyDigest($user, $notifications);
    }

    public function testSendEmailToAddresseeUser(): void
    {
        $user = new User();
        $user->setEmail('user@example.com');
        $notification = new Notification();
        $notification->setSender(new User());
        $notification->setTitle('Notification 1');
        $notification->setType('test_notification_type');
        $notification->addAddressee($user);

        $mailer = $this->prophesize(MailerInterface::class);
        $mailer->send(Argument::that(function ($arg) {
            if (!$arg instanceof Email) {
                return false;
            }

            if ('Notification 1' !== $arg->getSubject()) {
                return false;
            }

            foreach ($arg->getTo() as $address) {
                if ('user@example.com' === $address->getAddress()) {
                    return true;
                }
            }

            return false;
        }))->shouldBeCalledOnce();

        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());

        $notificationMailer->sendEmailToAddressee($notification, $user);
    }

    public function testSendEmailToAddresseeGroup(): void
    {
        $userGroup = new UserGroup();
        $userGroup->setEmail('user@example.com');
        $notification = new Notification();
        $notification->setSender(new User());
        $notification->setTitle('Notification 1');
        $notification->setType('test_notification_type');
        $notification->addAddressee($userGroup);

        $mailer = $this->prophesize(MailerInterface::class);
        $mailer->send(Argument::that(function ($arg) {
            if (!$arg instanceof Email) {
                return false;
            }

            if ('Notification 1' !== $arg->getSubject()) {
                return false;
            }

            foreach ($arg->getTo() as $address) {
                if ('user@example.com' === $address->getAddress()) {
                    return true;
                }
            }

            return false;
        }))->shouldBeCalledOnce();

        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());

        $notificationMailer->sendEmailToAddressee($notification, $userGroup);
    }

    public function testSendEmailToAddresseeGroupWithNoAddress(): void
    {
        $userGroup = new UserGroup();
        $notification = new Notification();
        $notification->setSender(new User());
        $notification->setTitle('Notification 1');
        $notification->setType('test_notification_type');
        $notification->addAddressee($userGroup);

        $mailer = $this->prophesize(MailerInterface::class);
        $mailer->send(Argument::any())->shouldNotBeCalled();

        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());

        $notificationMailer->sendEmailToAddressee($notification, $userGroup);
    }

    public function testSendEmailToAddresseeUserWithNoAddress(): void
    {
        $user = new User();
        $notification = new Notification();
        $notification->setSender(new User());
        $notification->setTitle('Notification 1');
        $notification->setType('test_notification_type');
        $notification->addAddressee($user);

        $mailer = $this->prophesize(MailerInterface::class);
        $mailer->send(Argument::any())->shouldNotBeCalled();

        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());

        $notificationMailer->sendEmailToAddressee($notification, $user);
    }

    private function buildNotificationMailer(
        ?MailerInterface $mailer = null,
        ?MessageBusInterface $messageBus = null,
        ?TranslatorInterface $translator = null,
    ): NotificationMailer {
        return new NotificationMailer(
            $mailer ?? $this->prophesize(MailerInterface::class)->reveal(),
            new NullLogger(),
            $messageBus ?? $this->prophesize(MessageBusInterface::class)->reveal(),
            $translator ?? new Translator('fr')
        );
    }
}
