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

use Ferienpass\CoreBundle\Comment\ReplyTokenSigner;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Uuid;

final class ReplyTokenSignerTest extends TestCase
{
    private const string SECRET = 'a-stable-secret';

    public function testSignedTokenIsLowercaseHexAndRoundTrips(): void
    {
        $signer = new ReplyTokenSigner(self::SECRET, 'reply.example.org');
        $uuid = Uuid::v7();

        $token = $signer->sign($this->itemWithUuid($uuid));

        self::assertMatchesRegularExpression('/^[0-9a-f]{32}\.[0-9a-f]{16}$/', $token, 'Token must be lower-case hex so it survives case-folding.');
        self::assertSame($uuid->toRfc4122(), $signer->verify($token)?->toRfc4122());
    }

    /**
     * The token rides in an email local-part (`kommentar+<token>@…`) that MTAs
     * and Brevo's inbound parser routinely case-fold. base58 was case-significant,
     * so a folded address silently failed verification — the inbound-reply bug.
     * Lower-case hex must round-trip regardless of the received case.
     */
    public function testVerifySurvivesCaseFolding(): void
    {
        $signer = new ReplyTokenSigner(self::SECRET, 'reply.example.org');
        $uuid = Uuid::v7();
        $token = $signer->sign($this->itemWithUuid($uuid));

        self::assertSame($uuid->toRfc4122(), $signer->verify(strtoupper($token))?->toRfc4122(), 'An upper-cased (folded) address must still verify.');
        self::assertSame($uuid->toRfc4122(), $signer->verify(strtolower($token))?->toRfc4122());
    }

    public function testVerifyFailsClosed(): void
    {
        $signer = new ReplyTokenSigner(self::SECRET, 'reply.example.org');
        $token = $signer->sign($this->itemWithUuid(Uuid::v7()));

        self::assertNull($signer->verify(''), 'Empty token.');
        self::assertNull($signer->verify('no-separator'), 'No "." separator.');
        // Flip the last hex character to a guaranteed-different one so the
        // tampering is never a no-op (a literal '0' would leave an already-'0'
        // last character unchanged, and the untampered token would verify).
        self::assertNull($signer->verify(substr($token, 0, -1).('0' === substr($token, -1) ? '1' : '0')), 'Tampered signature.');
        self::assertNull($signer->verify('zz'.substr($token, 2)), 'Non-hex payload.');
        self::assertNull(new ReplyTokenSigner('a-different-secret', 'reply.example.org')->verify($token), 'A token signed with a different secret.');
    }

    public function testReplyAddressStaysWithinTheLocalPartLimit(): void
    {
        $signer = new ReplyTokenSigner(self::SECRET, 'reply.demo.ferienpass.online');

        $address = $signer->replyAddress($this->itemWithUuid(Uuid::v7()));

        self::assertNotNull($address);
        [$localPart] = explode('@', $address);
        self::assertLessThanOrEqual(64, \strlen($localPart), 'The plus-address local-part must fit RFC 5321\'s 64-octet limit.');
    }

    private function itemWithUuid(Uuid $uuid): ConversationItem
    {
        $item = new \ReflectionClass(ConversationItem::class)->newInstanceWithoutConstructor();
        new \ReflectionProperty(ConversationItem::class, 'uuid')->setValue($item, $uuid);

        return $item;
    }
}
