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

use Doctrine\Common\Collections\ArrayCollection;
use Ferienpass\CoreBundle\ApplicationSystem\Lotting\Seat;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\EditionTask;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use PHPUnit\Framework\TestCase;

final class SeatTest extends TestCase
{
    public function testGetSeatsCountsAttendances(): void
    {
        $a1 = new Attendance(new Base(), new BaseParticipant());
        $a2 = new Attendance(new Base(), new BaseParticipant());
        $seat = new Seat([$a1, $a2]);

        self::assertSame(2, $seat->getSeats());
    }

    public function testGetAttendances(): void
    {
        $a1 = new Attendance(new Base(), new BaseParticipant());
        $seat = new Seat([$a1]);

        self::assertSame([$a1], $seat->getAttendances());
    }

    public function testSetStatusUpdatesAllAttendances(): void
    {
        $a1 = new Attendance(new Base(), new BaseParticipant());
        $a2 = new Attendance(new Base(), new BaseParticipant());
        $seat = new Seat([$a1, $a2]);

        $seat->setStatus(AttendanceStatus::CONFIRMED);

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

    public function testGetSeatsReturnsZeroForEmptySeat(): void
    {
        $seat = new Seat([]);

        self::assertSame(0, $seat->getSeats());
    }

    public function testGetValueReturnsZeroForEmptyAttendances(): void
    {
        $seat = new Seat([]);

        self::assertSame(0.0, $seat->getValue());
    }

    public function testGetValueReturnsZeroForFirstcomeTask(): void
    {
        $task = $this->createMock(EditionTask::class);
        $task->method('getType')->willReturn('firstcome');

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getTask')->willReturn($task);

        $seat = new Seat([$attendance]);

        self::assertSame(0.0, $seat->getValue());
    }

    public function testGetValueReturnsZeroWhenNoAttendancesInTheDraw(): void
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('isVariantOf')->willReturn(false);
        $offer->method('getMaxParticipants')->willReturn(10);
        $offer->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());

        $participant = $this->createMock(ParticipantInterface::class);
        $participant->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());
        $participant->method('getPostalCode')->willReturn(null);
        $participant->method('getAttendancesNotWithdrawn')->willReturn(new ArrayCollection());

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getTask')->willReturn(null);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getOffer')->willReturn($offer);

        $seat = new Seat([$attendance]);

        self::assertSame(0.0, $seat->getValue());
    }

    public function testGetValueReturnsZeroWhenParticipantHasConfirmedVariantAttendance(): void
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('isVariantOf')->willReturn(false);

        $confirmedAttendance = $this->createMock(Attendance::class);
        $confirmedAttendance->method('isConfirmed')->willReturn(true);
        $confirmedAttendance->method('getOffer')->willReturn($offer);

        // A variant offer that is confirmed
        $variantOffer = $this->createMock(OfferInterface::class);
        $variantOffer->method('isVariantOf')->with($offer)->willReturn(true);
        $confirmedVariantAttendance = $this->createMock(Attendance::class);
        $confirmedVariantAttendance->method('isConfirmed')->willReturn(true);
        $confirmedVariantAttendance->method('getOffer')->willReturn($variantOffer);

        $participant = $this->createMock(ParticipantInterface::class);
        $participant->method('getAttendancesNotHidden')->willReturn(new ArrayCollection([$confirmedVariantAttendance]));
        $participant->method('getPostalCode')->willReturn(null);

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getTask')->willReturn(null);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getOffer')->willReturn($offer);

        $seat = new Seat([$attendance]);

        self::assertSame(0.0, $seat->getValue());
    }

    public function testGetValueReturnsZeroForUnprivilegedPostalCode(): void
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('isVariantOf')->willReturn(false);
        $offer->method('getMaxParticipants')->willReturn(10);
        $offer->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());

        $participant = $this->createMock(ParticipantInterface::class);
        $participant->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());
        $participant->method('getPostalCode')->willReturn('99999'); // not in the allowed list
        $participant->method('getAttendancesNotWithdrawn')->willReturn(new ArrayCollection());

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getTask')->willReturn(null);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getOffer')->willReturn($offer);

        $seat = new Seat([$attendance], ['12345', '67890']); // allowed codes

        self::assertSame(0.0, $seat->getValue());
    }

    public function testGetValueReturnsZeroForMissingPostalCodeWhenListConfigured(): void
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('isVariantOf')->willReturn(false);
        $offer->method('getMaxParticipants')->willReturn(10);
        $offer->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());

        $participant = $this->createMock(ParticipantInterface::class);
        $participant->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());
        $participant->method('getPostalCode')->willReturn(null); // missing data must not bypass the gate
        $participant->method('getAttendancesNotWithdrawn')->willReturn(new ArrayCollection());

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getTask')->willReturn(null);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getOffer')->willReturn($offer);

        $seat = new Seat([$attendance], ['12345', '67890']); // allowed codes configured

        self::assertSame(0.0, $seat->getValue());
    }

    public function testGetValueIsNormalisedToAtMostOne(): void
    {
        // Top wish, sole (lost) application, popular offer: every fairness term maxes
        // out at 1.0, so the normalised, weight-summing score must be exactly 1.0.
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('isVariantOf')->willReturn(false);
        $offer->method('getMaxParticipants')->willReturn(5);
        $offer->method('getAttendancesNotHidden')->willReturn(new ArrayCollection(array_fill(0, 8, null)));
        $offer->method('getEdition')->willReturn(null);

        $task = $this->createMock(EditionTask::class);
        $task->method('getType')->willReturn('application_system');
        $task->method('getMaxApplications')->willReturn(0);

        $attendanceInDraw = $this->createMock(Attendance::class);
        $attendanceInDraw->method('getOffer')->willReturn($offer);
        $attendanceInDraw->method('getTask')->willReturn($task);
        $attendanceInDraw->method('getUserPriority')->willReturn(1);
        $attendanceInDraw->method('isWaitlisted')->willReturn(true);
        $attendanceInDraw->method('isUnfulfilled')->willReturn(false);

        $participant = $this->createMock(ParticipantInterface::class);
        $participant->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());
        $participant->method('getPostalCode')->willReturn(null);
        $participant->method('getAttendancesNotWithdrawn')->willReturn(new ArrayCollection([$attendanceInDraw]));

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getTask')->willReturn(null);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getOffer')->willReturn($offer);
        $attendance->method('getUserPriority')->willReturn(1);

        $seat = new Seat([$attendance]);

        self::assertEqualsWithDelta(1.0, $seat->getValue(), 0.0001);
    }

    public function testGetValueCalculatesWhenAttendancesInTheDraw(): void
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('isVariantOf')->willReturn(false);
        $offer->method('getMaxParticipants')->willReturn(5);
        $offer->method('getAttendancesNotHidden')->willReturn(new ArrayCollection(array_fill(0, 8, null))); // 8 > 5 = overbooked
        $offer->method('getEdition')->willReturn(null);

        $task = $this->createMock(EditionTask::class);
        $task->method('getType')->willReturn('application_system');
        $task->method('getMaxApplications')->willReturn(0);

        $attendanceInDraw = $this->createMock(Attendance::class);
        $attendanceInDraw->method('getOffer')->willReturn($offer);
        $attendanceInDraw->method('getTask')->willReturn($task);
        $attendanceInDraw->method('getUserPriority')->willReturn(1);
        $attendanceInDraw->method('isWaitlisted')->willReturn(true);
        $attendanceInDraw->method('isUnfulfilled')->willReturn(false);

        $participant = $this->createMock(ParticipantInterface::class);
        $participant->method('getAttendancesNotHidden')->willReturn(new ArrayCollection());
        $participant->method('getPostalCode')->willReturn(null);
        $participant->method('getAttendancesNotWithdrawn')->willReturn(new ArrayCollection([$attendanceInDraw]));

        $attendance = $this->createMock(Attendance::class);
        $attendance->method('getTask')->willReturn(null);
        $attendance->method('getParticipant')->willReturn($participant);
        $attendance->method('getOffer')->willReturn($offer);
        $attendance->method('getUserPriority')->willReturn(1);

        $seat = new Seat([$attendance]);

        $value = $seat->getValue();
        self::assertGreaterThan(0.0, $value);
    }
}
