<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Mime\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\MailboxListHeader;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
use Symfony\Component\Mime\Part\TextPart;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;

class MessageTest extends TestCase
{
    public function testConstruct()
    {
        $m = new Message();
        $this->assertNull($m->getBody());
        $this->assertEquals(new Headers(), $m->getHeaders());

        $m = new Message($h = (new Headers())->addDateHeader('Date', new \DateTimeImmutable()), $b = new TextPart('content'));
        $this->assertSame($b, $m->getBody());
        $this->assertEquals($h, $m->getHeaders());

        $m = new Message();
        $m->setBody($b);
        $m->setHeaders($h);
        $this->assertSame($b, $m->getBody());
        $this->assertSame($h, $m->getHeaders());
    }

    public function testGetPreparedHeadersThrowsWhenNoFrom()
    {
        $this->expectException(\LogicException::class);
        (new Message())->getPreparedHeaders();
    }

    public function testGetPreparedHeadersCloneHeaders()
    {
        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $this->assertNotSame($message->getPreparedHeaders(), $message->getHeaders());
    }

    public function testGetPreparedHeadersSetRequiredHeaders()
    {
        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $headers = $message->getPreparedHeaders();
        $this->assertTrue($headers->has('MIME-Version'));
        $this->assertTrue($headers->has('Message-ID'));
        $this->assertTrue($headers->has('Date'));
        $this->assertFalse($headers->has('Bcc'));
    }

    public function testGetPreparedHeaders()
    {
        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $h = $message->getPreparedHeaders();
        $this->assertCount(4, iterator_to_array($h->all()));
        $this->assertEquals(new MailboxListHeader('From', [new Address('fabien@symfony.com')]), $h->get('From'));
        $this->assertEquals(new UnstructuredHeader('MIME-Version', '1.0'), $h->get('mime-version'));
        $this->assertTrue($h->has('Message-Id'));
        $this->assertTrue($h->has('Date'));

        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $message->getHeaders()->addDateHeader('Date', $n = new \DateTimeImmutable());
        $this->assertEquals($n, $message->getPreparedHeaders()->get('Date')->getDateTime());

        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $message->getHeaders()->addMailboxListHeader('Bcc', ['fabien@symfony.com']);
        $this->assertNull($message->getPreparedHeaders()->get('Bcc'));
    }

    public function testGetPreparedHeadersWithNoFrom()
    {
        $this->expectException(\LogicException::class);
        (new Message())->getPreparedHeaders();
    }

    public function testGetPreparedHeadersWithNamedFrom()
    {
        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', [new Address('fabien@symfony.com', 'Fabien')]);
        $h = $message->getPreparedHeaders();
        $this->assertEquals(new MailboxListHeader('From', [new Address('fabien@symfony.com', 'Fabien')]), $h->get('From'));
        $this->assertTrue($h->has('Message-Id'));
    }

    public function testGetPreparedHeadersHasSenderWhenNeeded()
    {
        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $this->assertNull($message->getPreparedHeaders()->get('Sender'));

        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com', 'lucas@symfony.com']);
        $this->assertEquals('fabien@symfony.com', $message->getPreparedHeaders()->get('Sender')->getAddress()->getAddress());

        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com', 'lucas@symfony.com']);
        $message->getHeaders()->addMailboxHeader('Sender', 'thomas@symfony.com');
        $this->assertEquals('thomas@symfony.com', $message->getPreparedHeaders()->get('Sender')->getAddress()->getAddress());
    }

    public function testGenerateMessageIdThrowsWhenHasFromButNoAddresses()
    {
        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', []);
        $this->expectException(LogicException::class);
        $message->generateMessageId();
    }

    public function testGenerateMessageIdThrowsWhenNeitherFromNorSenderIsPresent()
    {
        $message = new Message();

        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('An email must have a "From" or a "Sender" header.');

        $message->generateMessageId();
    }

    public function testToString()
    {
        $message = new Message();
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $expected = <<<EOF
            From: fabien@symfony.com
            MIME-Version: 1.0
            Date: %s
            Message-ID: <%s@symfony.com>
            Content-Type: text/plain; charset=utf-8
            Content-Transfer-Encoding: quoted-printable


            EOF;
        $this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", $message->toString()));
        $this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", implode('', iterator_to_array($message->toIterable(), false))));

        $message = new Message(null, new TextPart('content'));
        $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
        $expected = <<<EOF
            From: fabien@symfony.com
            MIME-Version: 1.0
            Date: %s
            Message-ID: <%s@symfony.com>
            Content-Type: text/plain; charset=utf-8
            Content-Transfer-Encoding: quoted-printable

            content
            EOF;
        $this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", $message->toString()));
        $this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", implode('', iterator_to_array($message->toIterable(), false))));
    }

    public function testSymfonySerialize()
    {
        // we don't add from/sender to check that it's not needed to serialize an email
        $body = new MixedPart(
            new AlternativePart(
                new TextPart('Text content'),
                new TextPart('HTML content', 'utf-8', 'html')
            ),
            new DataPart('text data', 'text.txt')
        );
        $body->getHeaders()->addHeader('foo', 'bar');
        $e = new Message((new Headers())->addMailboxListHeader('To', ['you@example.com']), $body);
        $expected = clone $e;

        $expectedJson = <<<EOF
            {
                "headers": {
                    "to": [
                        {
                            "addresses": [
                                {
                                    "address": "you@example.com",
                                    "name": ""
                                }
                            ],
                            "name": "To",
                            "lineLength": 76,
                            "lang": null,
                            "charset": "utf-8"
                        }
                    ]
                },
                "body": {
                    "boundary": null,
                    "parts": [
                        {
                            "boundary": null,
                            "parts": [
                                {
                                    "body": "Text content",
                                    "charset": "utf-8",
                                    "subtype": "plain",
                                    "disposition": null,
                                    "name": null,
                                    "encoding": "quoted-printable",%A
                                    "headers": [],
                                    "class": "Symfony\\\\Component\\\\Mime\\\\Part\\\TextPart"
                                },
                                {
                                    "body": "HTML content",
                                    "charset": "utf-8",
                                    "subtype": "html",
                                    "disposition": null,
                                    "name": null,
                                    "encoding": "quoted-printable",%A
                                    "headers": [],
                                    "class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\TextPart"
                                }
                            ],
                            "headers": [],
                            "class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\Multipart\\\\AlternativePart"
                        },
                        {
                            "filename": "text.txt",
                            "mediaType": "application",%A
                            "body": "text data",
                            "charset": null,
                            "subtype": "octet-stream",
                            "disposition": "attachment",
                            "name": "text.txt",
                            "encoding": "base64",%A
                            "headers": [],
                            "class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\DataPart"
                        }
                    ],
                    "headers": {
                        "foo": [
                            {
                                "value": "bar",
                                "name": "foo",
                                "lineLength": 76,
                                "lang": null,
                                "charset": "utf-8"
                            }
                        ]
                    },
                    "class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\Multipart\\\\MixedPart"
                }
            }
            EOF;

        $extractor = new PhpDocExtractor();
        $propertyNormalizer = new PropertyNormalizer(null, null, $extractor);
        $serializer = new Serializer([
            new ArrayDenormalizer(),
            new MimeMessageNormalizer($propertyNormalizer),
            new ObjectNormalizer(null, null, null, $extractor),
            $propertyNormalizer,
        ], [new JsonEncoder()]);

        $serialized = $serializer->serialize($e, 'json');
        $this->assertStringMatchesFormat($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));

        $n = $serializer->deserialize($serialized, Message::class, 'json');
        $this->assertEquals($expected, $n);

        $serialized = $serializer->serialize($e, 'json');
        $this->assertStringMatchesFormat($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
    }

    #[DataProvider('ensureValidityProvider')]
    public function testEnsureValidity(array $headers, ?string $exceptionClass, ?string $exceptionMessage)
    {
        if ($exceptionClass) {
            $this->expectException($exceptionClass);
            $this->expectExceptionMessage($exceptionMessage);
        } else {
            $this->expectNotToPerformAssertions();
        }

        $m = new Message();
        foreach ($headers as $headerName => $headerValue) {
            $m->getHeaders()->addMailboxListHeader($headerName, $headerValue);
        }
        $m->ensureValidity();
    }

    public static function ensureValidityProvider(): array
    {
        return [
            'Valid address fields' => [
                [
                    'To' => ['dummy@symfony.com'],
                    'From' => ['test@symfony.com'],
                ],
                null,
                null,
            ],

            'No destination address fields' => [
                [
                    'From' => ['test@symfony.com'],
                ],
                LogicException::class,
                'An email must have a "To", "Cc", or "Bcc" header.',
            ],

            'Empty destination address fields' => [
                [
                    'To' => [],
                    'From' => ['test@symfony.com'],
                ],
                LogicException::class,
                'An email must have a "To", "Cc", or "Bcc" header.',
            ],

            'No originator fields' => [
                [
                    'To' => ['dummy@symfony.com'],
                ],
                LogicException::class,
                'An email must have a "From" or a "Sender" header.',
            ],

            'Empty originator fields' => [
                [
                    'To' => ['dummy@symfony.com'],
                    'From' => [],
                ],
                LogicException::class,
                'An email must have a "From" or a "Sender" header.',
            ],
        ];
    }
}
