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

use Ferienpass\CoreBundle\ApplicationSystem\FirstComeApplicationSystem;
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\Fixtures\Factory\AttendanceFactory;
use Ferienpass\CoreBundle\Fixtures\Factory\EditionTaskFactory;
use Ferienpass\CoreBundle\Fixtures\Factory\OfferFactory;
use PHPUnit\Framework\TestCase;
use Zenstruck\Foundry\Test\Factories;

class FirstComeApplicationSystemTest extends TestCase
{
    use Factories;

    public function testGetType(): void
    {
        self::assertSame('firstcome', FirstComeApplicationSystem::getType());
    }

    public function testSuggestsConfirmWhenNoLimit(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        $attendance = new Attendance(new Base(), new BaseParticipant());

        self::assertSame(AttendanceTransition::CONFIRM, $applicationSystem->suggestTransition($attendance));
    }

    public function testDoesNothingForNonPendingStatus(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        $attendance = new Attendance(new Base(), new BaseParticipant());
        $attendance->setStatus(AttendanceStatus::WITHDRAWN);
        self::assertNull($applicationSystem->suggestTransition($attendance));

        $attendance = new Attendance(new Base(), new BaseParticipant());
        $attendance->setStatus(AttendanceStatus::CONFIRMED);
        self::assertNull($applicationSystem->suggestTransition($attendance));
    }

    public function testSuggestsConfirmForWaitlistedWithoutLimit(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        $attendance = new Attendance(new Base(), new BaseParticipant());
        $attendance->setStatus(AttendanceStatus::WAITLISTED);

        self::assertSame(AttendanceTransition::CONFIRM, $applicationSystem->suggestTransition($attendance));
    }

    public function testSuggestsConfirmWhenSpotAvailable(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        $confirmedAttendances = AttendanceFactory::new()->withStatus('confirmed')->many(4)->create();
        $offer = OfferFactory::new()->withMaxParticipants(5)->withAttendances($confirmedAttendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::CONFIRM, $applicationSystem->suggestTransition($attendance));
    }

    public function testSuggestsWaitlistWhenFull(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        $confirmedAttendances = AttendanceFactory::new()->withStatus('confirmed')->many(4)->create();
        $offer = OfferFactory::new()->withMaxParticipants(4)->withAttendances($confirmedAttendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::WAITLIST, $applicationSystem->suggestTransition($attendance));
    }

    public function testRefusesInsteadOfWaitlistingWhenSeasonKeepsNoWaitlist(): void
    {
        $applicationSystem = $this->makeApplicationSystem(maxWaitlist: 0);

        $confirmedAttendances = AttendanceFactory::new()->withStatus('confirmed')->many(4)->create();
        $offer = OfferFactory::new()->withMaxParticipants(4)->withAttendances($confirmedAttendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::UNFULFILL, $applicationSystem->suggestTransition($attendance));
    }

    public function testWaitlistsUpToTheSeasonCap(): void
    {
        $applicationSystem = $this->makeApplicationSystem(maxWaitlist: 2);

        $attendances = [
            ...AttendanceFactory::new()->withStatus('confirmed')->many(4)->create(),
            AttendanceFactory::new()->withStatus('waitlisted')->create(),
        ];
        $offer = OfferFactory::new()->withMaxParticipants(4)->withAttendances($attendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::WAITLIST, $applicationSystem->suggestTransition($attendance));
    }

    public function testRefusesOnceTheSeasonWaitlistCapIsReached(): void
    {
        $applicationSystem = $this->makeApplicationSystem(maxWaitlist: 2);

        $attendances = [
            ...AttendanceFactory::new()->withStatus('confirmed')->many(4)->create(),
            ...AttendanceFactory::new()->withStatus('waitlisted')->many(2)->create(),
        ];
        $offer = OfferFactory::new()->withMaxParticipants(4)->withAttendances($attendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::UNFULFILL, $applicationSystem->suggestTransition($attendance));
    }

    /**
     * A re-evaluation (ReorderParticipantList runs on every application) must
     * never retroactively drop somebody off a waiting list they are already on,
     * not even when the season meanwhile switched the waiting list off.
     */
    public function testLeavesAlreadyWaitlistedAttendancesAloneWhenStillFull(): void
    {
        $applicationSystem = $this->makeApplicationSystem(maxWaitlist: 0);

        $confirmedAttendances = AttendanceFactory::new()->withStatus('confirmed')->many(4)->create();
        $offer = OfferFactory::new()->withMaxParticipants(4)->withAttendances($confirmedAttendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());
        $attendance->setStatus(AttendanceStatus::WAITLISTED);

        self::assertNull($applicationSystem->suggestTransition($attendance));
    }

    public function testWaitlistsWithoutATaskAsBefore(): void
    {
        $confirmedAttendances = AttendanceFactory::new()->withStatus('confirmed')->many(4)->create();
        $offer = OfferFactory::new()->withMaxParticipants(4)->withAttendances($confirmedAttendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::WAITLIST, new FirstComeApplicationSystem()->suggestTransition($attendance));
    }

    public function testSuggestsConfirmForExistingWaitlistedParticipantWithSpotLeft(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        $confirmedAttendances = AttendanceFactory::new()->withStatus('confirmed')->many(4)->create();
        $offer = OfferFactory::new()->withMaxParticipants(5)->withAttendances($confirmedAttendances)->create();

        // Attendance is already in the offer's collection at position 4, max is 5 → confirm
        $participant = new BaseParticipant();
        $existing = new Attendance($offer, $participant);
        $existing->setStatus(AttendanceStatus::CONFIRMED);

        $allAttendances = [...$confirmedAttendances, $existing];
        $offer = OfferFactory::new()->withMaxParticipants(5)->withAttendances($allAttendances)->create();

        $waitlisted = new Attendance($offer, $participant);
        $waitlisted->setStatus(AttendanceStatus::WAITLISTED);

        self::assertSame(AttendanceTransition::CONFIRM, $applicationSystem->suggestTransition($waitlisted));
    }

    public function testSuggestsConfirmWhenNoExistingConfirmedAndSpotAvailable(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        // Offer has a max limit but zero confirmed/waitlisted attendances
        $offer = OfferFactory::new()->withMaxParticipants(3)->withAttendances([])->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::CONFIRM, $applicationSystem->suggestTransition($attendance));
    }

    public function testIgnoresErrorStatusWhenCountingFilledSpots(): void
    {
        $applicationSystem = $this->makeApplicationSystem();

        // Offer with max 3; 2 confirmed + 1 error + 1 withdrawn — only 2 count toward the limit
        $attendances = [
            AttendanceFactory::new()->withStatus('confirmed')->create(),
            AttendanceFactory::new()->withStatus('confirmed')->create(),
            AttendanceFactory::new()->withStatus('error')->create(),
            AttendanceFactory::new()->withStatus('withdrawn')->create(),
        ];
        $offer = OfferFactory::new()->withMaxParticipants(3)->withAttendances($attendances)->create();

        $attendance = new Attendance($offer, new BaseParticipant());

        self::assertSame(AttendanceTransition::CONFIRM, $applicationSystem->suggestTransition($attendance));
    }

    private function makeApplicationSystem(?int $maxWaitlist = null): FirstComeApplicationSystem
    {
        $editionTask = EditionTaskFactory::new()->ofTypeFirstComeApplicationSystem()->withMaxWaitlist($maxWaitlist)->create();

        return new FirstComeApplicationSystem()->withTask($editionTask);
    }
}
