<?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\ConversationVisibility;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\User;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;

/**
 * What a Veranstalter may read in a conversation. The team writes internal notes
 * in the same threads a host takes part in, so anything not addressed to that
 * host — and not written by them — must stay out of their view.
 */
class ConversationVisibilityTest extends TestCase
{
    public function testTheTeamSeesEverything(): void
    {
        $comment = $this->comment($this->user(7), []);

        self::assertTrue($this->visibility(admin: true)->isVisible($comment));
    }

    public function testAHostSeesWhatWasAddressedToTheirOwnHost(): void
    {
        $me = $this->user(7, hosts: [42]);
        $comment = $this->comment(null, [['type' => 'group', 'g' => 'host', 'id' => 42, 'label' => 'VHS']]);

        self::assertTrue($this->visibility(user: $me)->isVisible($comment));
    }

    public function testAHostDoesNotSeeWhatWasAddressedToAnotherHost(): void
    {
        // The offer has two Veranstalter; a message to one is not for the other.
        // `isInternal()` would call this comment "shared" and let it through.
        $me = $this->user(7, hosts: [42]);
        $comment = $this->comment(null, [['type' => 'group', 'g' => 'host', 'id' => 99, 'label' => 'Andere']]);

        self::assertFalse($comment->isInternal(), 'precondition: the coarse flag calls this shared');
        self::assertFalse($this->visibility(user: $me)->isVisible($comment));
    }

    public function testAHostDoesNotSeeWhatWasSharedWithTheFamily(): void
    {
        $me = $this->user(7, hosts: [42]);
        $comment = $this->comment(null, [['type' => 'group', 'g' => 'account', 'label' => 'Familie Müller']]);

        self::assertFalse($comment->isInternal(), 'precondition: the coarse flag calls this shared');
        self::assertFalse($this->visibility(user: $me)->isVisible($comment));
    }

    public function testAHostDoesNotSeeAnInternalTeamNote(): void
    {
        $me = $this->user(7, hosts: [42]);
        $comment = $this->comment(null, [['type' => 'group', 'g' => 'admin', 'label' => 'Team']]);

        self::assertFalse($this->visibility(user: $me)->isVisible($comment));
    }

    public function testAHostSeesTheirOwnMessage(): void
    {
        $me = $this->user(7, hosts: [42]);

        self::assertTrue($this->visibility(user: $me)->isVisible($this->comment($me, [])));
    }

    public function testAHostSeesTheirOwnEmailReplyEvenWhenItWasNeverLinkedToTheAccount(): void
    {
        // An inbound reply we could not resolve to a User carries only the From
        // address — the sender must still find their own message in the thread.
        $me = $this->user(7, hosts: [42], email: 'kontakt@vhs.example');

        $comment = new ConversationItem(new Base(), null, 'Passt so.', authorEmail: 'Kontakt@VHS.example');

        self::assertTrue($this->visibility(user: $me)->isVisible($comment));
    }

    public function testAnIdLessHostTargetStillAddressesTheHost(): void
    {
        // The legacy descriptor fans out to every host of the subject, and a host
        // only reaches the subject as one of them.
        $me = $this->user(7, hosts: [42]);
        $comment = $this->comment(null, [['type' => 'group', 'g' => 'host', 'label' => 'Veranstalter']]);

        self::assertTrue($this->visibility(user: $me)->isVisible($comment));
    }

    public function testAReplyInheritsTheAudienceOfTheThreadItAnswers(): void
    {
        // The team answers the host's question without re-picking the audience.
        // Without inheritance the host would be left looking at their own message
        // with the answer to it missing.
        $me = $this->user(7, hosts: [42]);
        $parent = $this->comment(null, [['type' => 'group', 'g' => 'host', 'id' => 42]]);
        $reply = new ConversationItem(new Base(), null, 'Ja, passt.', parent: $parent);

        self::assertTrue($this->visibility(user: $me)->isVisible($reply));
    }

    public function testAReplyInAThreadThatIsNotTheirsStaysHidden(): void
    {
        $me = $this->user(7, hosts: [42]);
        $parent = $this->comment(null, [['type' => 'group', 'g' => 'host', 'id' => 99]]);
        $reply = new ConversationItem(new Base(), null, 'Intern geklärt.', parent: $parent);

        self::assertFalse($this->visibility(user: $me)->isVisible($reply));
    }

    public function testAnAnonymousViewerSeesNothing(): void
    {
        self::assertFalse($this->visibility(user: null)->isVisible($this->comment(null, [])));
    }

    public function testFilterKeepsOrderAndDropsTheRest(): void
    {
        $me = $this->user(7, hosts: [42]);
        $mine = $this->comment(null, [['type' => 'group', 'g' => 'host', 'id' => 42]]);
        $theirs = $this->comment(null, [['type' => 'group', 'g' => 'admin']]);
        $alsoMine = $this->comment($me, []);

        $visible = $this->visibility(user: $me)->filter([$mine, $theirs, $alsoMine]);

        self::assertSame([$mine, $alsoMine], $visible);
    }

    private function visibility(bool $admin = false, ?User $user = null): ConversationVisibility
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->willReturn($admin);
        $security->method('getUser')->willReturn($user);

        return new ConversationVisibility($security);
    }

    /**
     * @param array<int, array<string, mixed>> $notify
     */
    private function comment(?User $author, array $notify): ConversationItem
    {
        return new ConversationItem(new Base(), $author, 'Nachricht', notify: $notify);
    }

    /**
     * @param list<int> $hosts
     */
    private function user(int $id, array $hosts = [], string $email = 'host@example.org'): User
    {
        $user = new User();
        $user->setEmail($email);

        $reflection = new \ReflectionProperty(User::class, 'id');
        $reflection->setValue($user, $id);

        foreach ($hosts as $hostId) {
            $host = new Host();
            new \ReflectionProperty(Host::class, 'id')->setValue($host, $hostId);
            $user->addHost($host);
        }

        return $user;
    }
}
