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

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\ApplicationSystem\ApplicationSystemInterface;
use Ferienpass\CoreBundle\ApplicationSystem\ApplicationSystems;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Enum\AttendanceTransition;
use Ferienpass\CoreBundle\Facade\AttendanceFacade;
use Ferienpass\CoreBundle\Repository\AttendanceRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\SharedLockInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Workflow\WorkflowInterface;

final class AttendanceFacadeTest extends TestCase
{
    public function testPreviewReturnsAttendanceInstance(): void
    {
        $offer = new Base();
        $participant = new BaseParticipant();

        $appSystems = $this->createMock(ApplicationSystems::class);
        $appSystems->method('findApplicationSystem')->willReturn(null);

        $facade = $this->makeFacade($appSystems);
        $attendance = $facade->preview($offer, $participant);

        self::assertInstanceOf(Attendance::class, $attendance);
        self::assertSame($offer, $attendance->getOffer());
        self::assertSame($participant, $attendance->getParticipant());
        // No application system → the projection leaves it in the initial WAITING state.
        self::assertSame(AttendanceStatus::WAITING, $attendance->getStatus());
    }

    /**
     * preview() projects the application system's suggested transition onto the
     * attendance status, so the admin dialog can pre-select the status the applicant
     * would actually get (e.g. "bestätigt" when a seat is free) instead of always "neu".
     */
    public function testPreviewProjectsSuggestedTransitionOntoStatus(): void
    {
        $applicationSystem = $this->createMock(ApplicationSystemInterface::class);
        $applicationSystem->method('suggestTransition')->willReturn(AttendanceTransition::CONFIRM);

        $appSystems = $this->createMock(ApplicationSystems::class);
        $appSystems->method('findApplicationSystem')->willReturn($applicationSystem);

        $facade = $this->makeFacade($appSystems);
        $attendance = $facade->preview(new Base(), new BaseParticipant());

        self::assertSame(AttendanceStatus::CONFIRMED, $attendance->getStatus());
    }

    public function testCreateThrowsWhenNotifyTrueButCommitFalse(): void
    {
        $this->expectException(\RuntimeException::class);

        $facade = $this->makeFacade();
        $facade->create(new Base(), new BaseParticipant(), notify: true, commit: false);
    }

    /**
     * An explicit "neu" (waiting) status is an admin decision to park the applicant
     * in the waiting column. It must NOT trigger the application system, otherwise
     * FirstCome would immediately confirm the attendance whenever a seat is free.
     */
    public function testCreateWithWaitingStatusHoldsInsteadOfAllocating(): void
    {
        $attendances = $this->createMock(AttendanceRepository::class);
        $attendances->method('findExisting')->willReturn(null);

        $appSystems = $this->createMock(ApplicationSystems::class);
        $appSystems->expects(self::never())->method('findApplicationSystem');

        $facade = $this->makeFacade($appSystems, $attendances);

        $attendance = $facade->create(new Base(), new BaseParticipant(), status: AttendanceStatus::WAITING, notify: false, commit: false);

        self::assertNotNull($attendance);
        self::assertSame(AttendanceStatus::WAITING, $attendance->getStatus());
    }

    /**
     * Without an explicit status (the public application form) the application
     * system still decides the allocation, guarded by the per-offer lock.
     */
    public function testCreateWithoutStatusRunsApplicationSystem(): void
    {
        $attendances = $this->createMock(AttendanceRepository::class);
        $attendances->method('findExisting')->willReturn(null);

        $applicationSystem = $this->createMock(ApplicationSystemInterface::class);
        $applicationSystem->method('suggestTransition')->willReturn(null);

        $appSystems = $this->createMock(ApplicationSystems::class);
        $appSystems->expects(self::once())->method('findApplicationSystem')->willReturn($applicationSystem);

        // LockFactory::createLock() declares SharedLockInterface as its return type.
        $lock = $this->createMock(SharedLockInterface::class);
        $lockFactory = $this->createMock(LockFactory::class);
        $lockFactory->method('createLock')->willReturn($lock);

        $facade = $this->makeFacade($appSystems, $attendances, lockFactory: $lockFactory);

        $attendance = $facade->create(new Base(), new BaseParticipant(), notify: false);

        self::assertNotNull($attendance);
    }

    public function testIncreasePriorityDoesNothingWhenNotWaiting(): void
    {
        $offer = new Base();
        $participant = new BaseParticipant();
        $attendance = new Attendance($offer, $participant);
        $attendance->setStatus(AttendanceStatus::CONFIRMED);

        $em = $this->createMock(EntityManagerInterface::class);
        $em->expects(self::never())->method('flush');

        $facade = $this->makeFacade(em: $em);

        $facade->increasePriority($attendance);
    }

    private function makeFacade(
        ?ApplicationSystems $appSystems = null,
        ?AttendanceRepository $attendances = null,
        ?EntityManagerInterface $em = null,
        ?LockFactory $lockFactory = null,
    ): AttendanceFacade {
        $bus = $this->createMock(MessageBusInterface::class);
        $bus->method('dispatch')->willReturn(new Envelope(new \stdClass()));

        return new AttendanceFacade(
            $bus,
            $em ?? $this->createMock(EntityManagerInterface::class),
            $appSystems ?? $this->createMock(ApplicationSystems::class),
            $attendances ?? $this->createMock(AttendanceRepository::class),
            $this->createMock(WorkflowInterface::class),
            $lockFactory ?? $this->createMock(LockFactory::class),
        );
    }
}
