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

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Ferienpass\CoreBundle\ApplicationSystem\ApplicationSystemInterface;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\ParticipantLog;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Enum\AttendanceTransition;
use Ferienpass\CoreBundle\Enum\DecisionReason;
use Ferienpass\CoreBundle\EventListener\Doctrine\Attendance\AttendanceLogListener;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;

final class AttendanceLogListenerTest extends TestCase
{
    public function testPreUpdateReturnsEarlyWhenStatusFieldNotChanged(): void
    {
        $security = $this->createMock(Security::class);
        $security->expects(self::never())->method('getUser');

        $attendance = $this->createMock(Attendance::class);
        $attendance->expects(self::never())->method('getActivity');

        $changeSet = ['name' => ['old_name', 'new_name']];
        $args = new PreUpdateEventArgs($attendance, $this->createMock(EntityManagerInterface::class), $changeSet);

        $listener = new AttendanceLogListener($security);
        $listener->preUpdate($attendance, $args);
    }

    public function testPreUpdateReturnsEarlyWhenNewStatusIsNull(): void
    {
        $security = $this->createMock(Security::class);
        $security->expects(self::never())->method('getUser');

        $attendance = $this->createMock(Attendance::class);
        $attendance->expects(self::never())->method('getActivity');

        $changeSet = ['status' => ['old_status', null]];
        $args = new PreUpdateEventArgs($attendance, $this->createMock(EntityManagerInterface::class), $changeSet);

        $listener = new AttendanceLogListener($security);
        $listener->preUpdate($attendance, $args);
    }

    public function testPreUpdateLogsUnfulfilledTransition(): void
    {
        // A manual (non-lotting) status change to unfulfilled now produces a log row;
        // previously unfulfilled was unmapped and left no trace.
        $log = $this->dispatchPreUpdate(AttendanceStatus::WAITING, AttendanceStatus::UNFULFILLED);

        self::assertSame(AttendanceTransition::UNFULFILL, $log->getTransitionName());
        self::assertNull($log->getApplicationSystem(), 'A manual change is not attributed to a system');
        self::assertNull($log->getDecisionReason());
    }

    public function testPreUpdateAttributesManualChangeToSecurityUser(): void
    {
        $user = $this->createMock(User::class);
        $log = $this->dispatchPreUpdate(AttendanceStatus::WAITING, AttendanceStatus::CONFIRMED, user: $user);

        self::assertSame($user, $log->getCreatedBy());
        self::assertNull($log->getApplicationSystem());
    }

    public function testPreUpdateLottingDecisionStampsActorAndReason(): void
    {
        // A lottery decision is attributed to the "Losverfahren" system (no user) and
        // carries the decision reason.
        $log = $this->dispatchPreUpdate(
            AttendanceStatus::WAITING,
            AttendanceStatus::UNFULFILLED,
            byLot: true,
            reason: DecisionReason::WAITLIST_EXHAUSTED,
        );

        self::assertNull($log->getCreatedBy(), 'Lotting has no user actor');
        self::assertSame(ApplicationSystemInterface::LOT, $log->getApplicationSystem());
        self::assertSame(DecisionReason::WAITLIST_EXHAUSTED, $log->getDecisionReason());
        self::assertSame(AttendanceTransition::UNFULFILL, $log->getTransitionName());
    }

    /**
     * Runs preUpdate for a status change and returns the single ParticipantLog it appended.
     */
    private function dispatchPreUpdate(string $from, string $to, bool $byLot = false, ?string $reason = null, ?User $user = null): ParticipantLog
    {
        $security = $this->createMock(Security::class);
        if ($byLot) {
            $security->expects(self::never())->method('getUser');
        } else {
            $security->method('getUser')->willReturn($user);
        }

        $activity = new ArrayCollection();

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getActivity')->willReturn($activity);
        $attendance->method('getParticipant')->willReturn(null);
        $attendance->method('isLottingActor')->willReturn($byLot);
        $attendance->method('getLottingDecisionReason')->willReturn($reason);

        $changeSet = ['status' => [$from, $to]];
        $args = new PreUpdateEventArgs($attendance, $this->createMock(EntityManagerInterface::class), $changeSet);

        new AttendanceLogListener($security)->preUpdate($attendance, $args);

        self::assertCount(1, $activity);

        return $activity->first();
    }
}
