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

use Ferienpass\CoreBundle\Entity\AccessCode;
use Ferienpass\CoreBundle\Entity\AccessCodeStrategy;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\FriendCode;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\ParticipantDuplicate;
use Ferienpass\CoreBundle\Entity\PostalAddress;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use PHPUnit\Framework\TestCase;

final class BaseParticipantTest extends TestCase
{
    public function testGetNameCombinesFirstAndLast(): void
    {
        $participant = new BaseParticipant();
        $participant->setFirstname('Anna');
        $participant->setLastname('Smith');

        self::assertSame('Anna Smith', $participant->getName());
    }

    public function testGetNameTrimsWhenMissingParts(): void
    {
        $participant = new BaseParticipant();
        $participant->setFirstname('Anna');

        self::assertSame('Anna', $participant->getName());
    }

    public function testGetAgeCalculatesFromDateOfBirth(): void
    {
        $participant = new BaseParticipant();
        $dob = new \DateTimeImmutable('-10 years');
        $participant->setDateOfBirth($dob);

        self::assertSame(10, $participant->getAge());
    }

    public function testGetAgeReturnsNullWhenNoBirthday(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getAge());
    }

    public function testGetAgeAtSpecificDate(): void
    {
        $participant = new BaseParticipant();
        $participant->setDateOfBirth(new \DateTimeImmutable('2000-06-15'));

        $atDate = new \DateTimeImmutable('2018-01-01');
        self::assertSame(17, $participant->getAge($atDate));
    }

    public function testGetEmailReturnsOwnEmail(): void
    {
        $participant = new BaseParticipant();
        $participant->setEmail('own@example.com');

        self::assertSame('own@example.com', $participant->getEmail());
    }

    public function testGetEmailFallsBackToUserEmail(): void
    {
        $user = new User();
        $user->setEmail('user@example.com');

        $participant = new BaseParticipant($user);

        self::assertSame('user@example.com', $participant->getEmail());
    }

    public function testGetEmailReturnsNullWhenNoUserAndNoOwnEmail(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getEmail());
    }

    public function testGetPhoneReturnsOwnPhone(): void
    {
        $participant = new BaseParticipant();
        $participant->setPhone('+49 30 12345');

        self::assertSame('+49 30 12345', $participant->getPhone());
    }

    public function testGetPhoneFallsBackToUserPhone(): void
    {
        $user = new User();
        $user->setPhone('+49 30 99999');

        $participant = new BaseParticipant($user);

        self::assertSame('+49 30 99999', $participant->getPhone());
    }

    public function testGetPhoneReturnsNullWhenNoUserAndNoOwnPhone(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getPhone());
    }

    public function testGetMobileFallsBackToUserMobile(): void
    {
        $user = new User();
        $user->setMobile('+49 170 12345');

        $participant = new BaseParticipant($user);

        self::assertSame('+49 170 12345', $participant->getMobile());
    }

    public function testGetUser(): void
    {
        $user = new User();
        $participant = new BaseParticipant($user);

        self::assertSame($user, $participant->getUser());
    }

    public function testSetUser(): void
    {
        $participant = new BaseParticipant();
        $user = new User();
        $participant->setUser($user);

        self::assertSame($user, $participant->getUser());
    }

    public function testUuidIsGeneratedOnConstruct(): void
    {
        $p1 = new BaseParticipant();
        $p2 = new BaseParticipant();

        self::assertNotSame((string) $p1->getUuid(), (string) $p2->getUuid());
    }

    public function testAttendancesInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendances());
    }

    public function testGetOwnPhoneReturnsNull(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getOwnPhone());
    }

    public function testGetOwnPhoneReturnsSetValue(): void
    {
        $participant = new BaseParticipant();
        $participant->setPhone('+49 30 12345');

        self::assertSame('+49 30 12345', $participant->getOwnPhone());
    }

    public function testSetOwnPhone(): void
    {
        $participant = new BaseParticipant();
        $participant->setOwnPhone('+49 30 99999');

        self::assertSame('+49 30 99999', $participant->getPhone());
    }

    public function testGetOwnMobileReturnsNull(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getOwnMobile());
    }

    public function testSetOwnMobile(): void
    {
        $participant = new BaseParticipant();
        $participant->setOwnMobile('+49 170 99999');

        self::assertSame('+49 170 99999', $participant->getMobile());
    }

    public function testGetOwnEmailReturnsNull(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getOwnEmail());
    }

    public function testSetOwnEmail(): void
    {
        $participant = new BaseParticipant();
        $participant->setOwnEmail('own@example.com');

        self::assertSame('own@example.com', $participant->getEmail());
    }

    public function testGetDateOfBirth(): void
    {
        $participant = new BaseParticipant();
        $dob = new \DateTimeImmutable('2000-01-15');
        $participant->setDateOfBirth($dob);

        self::assertSame($dob, $participant->getDateOfBirth());
    }

    public function testGetDateOfBirthNullByDefault(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getDateOfBirth());
    }

    public function testGetFirstname(): void
    {
        $participant = new BaseParticipant();
        $participant->setFirstname('Alice');

        self::assertSame('Alice', $participant->getFirstname());
    }

    public function testGetLastname(): void
    {
        $participant = new BaseParticipant();
        $participant->setLastname('Wonderland');

        self::assertSame('Wonderland', $participant->getLastname());
    }

    public function testGetPostalAddress(): void
    {
        $participant = new BaseParticipant();
        $address = new PostalAddress();
        $participant->setPostalAddress($address);

        self::assertSame($address, $participant->getPostalAddress());
    }

    public function testGetPostalAddressNullByDefault(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getPostalAddress());
    }

    public function testGetCreatedAt(): void
    {
        $before = new \DateTimeImmutable();
        $participant = new BaseParticipant();
        $after = new \DateTimeImmutable();

        self::assertGreaterThanOrEqual($before, $participant->getCreatedAt());
        self::assertLessThanOrEqual($after, $participant->getCreatedAt());
    }

    public function testConsentsInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getConsents());
    }

    public function testAccessCodesInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAccessCodes());
    }

    public function testFriendCodesInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getFriendCodes());
    }

    public function testDuplicatesInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getDuplicates());
    }

    public function testActivityInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getActivity());
    }

    public function testGetAttendancesByStatusConfirmedReturnsEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesByStatus(AttendanceStatus::CONFIRMED));
    }

    public function testGetAttendancesByStatusThrowsForUnknownStatus(): void
    {
        $participant = new BaseParticipant();

        $this->expectException(\InvalidArgumentException::class);
        $participant->getAttendancesByStatus('invalid_status');
    }

    public function testGetAttendancesConfirmedReturnsEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesConfirmed());
    }

    public function testGetAttendancesWaitlistedReturnsEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesWaitlisted());
    }

    public function testGetAttendancesWaitingReturnsEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesWaiting());
    }

    public function testGetAttendancesRejectedReturnsEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesRejected());
    }

    public function testGetAttendancesUnfulfilledReturnsEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesUnfulfilled());
    }

    public function testGetAttendancesPaidReturnsEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesPaid());
    }

    public function testGetAttendancesConfirmedCountIsZero(): void
    {
        $participant = new BaseParticipant();

        self::assertSame(0, $participant->getAttendancesConfirmedCount());
    }

    public function testGetAttendancesWaitlistedCountIsZero(): void
    {
        $participant = new BaseParticipant();

        self::assertSame(0, $participant->getAttendancesWaitlistedCount());
    }

    public function testGetAttendancesWaitingCountIsZero(): void
    {
        $participant = new BaseParticipant();

        self::assertSame(0, $participant->getAttendancesWaitingCount());
    }

    public function testGetAttendancesRejectedCountIsZero(): void
    {
        $participant = new BaseParticipant();

        self::assertSame(0, $participant->getAttendancesRejectedCount());
    }

    public function testGetAttendancesPaidCountIsZero(): void
    {
        $participant = new BaseParticipant();

        self::assertSame(0, $participant->getAttendancesPaidCount());
    }

    public function testHasUnpaidAttendancesReturnsFalseWhenEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertFalse($participant->hasUnpaidAttendances());
    }

    public function testGetPostalCodeNullWhenNoAddressAndNoUser(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getPostalCode());
    }

    public function testGetConsentNullWhenNoConsents(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getConsent('some_type'));
    }

    public function testGetLastAttendanceNullWhenEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getLastAttendance());
    }

    public function testGetIdNullByDefault(): void
    {
        $participant = new BaseParticipant();

        self::assertNull($participant->getId());
    }

    public function testSetPhone(): void
    {
        $participant = new BaseParticipant();
        $participant->setPhone('0123456789');

        self::assertSame('0123456789', $participant->getOwnPhone());
    }

    public function testSetMobile(): void
    {
        $participant = new BaseParticipant();
        $participant->setMobile('0176123456');

        self::assertSame('0176123456', $participant->getOwnMobile());
    }

    public function testSetEmail(): void
    {
        $participant = new BaseParticipant();
        $participant->setEmail('test@example.com');

        self::assertSame('test@example.com', $participant->getOwnEmail());
    }

    public function testGetAttendancesNotWithdrawnEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesNotWithdrawn());
    }

    public function testGetAttendancesNotHiddenEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getAttendancesNotHidden());
    }

    public function testGetUnpaidSumIsZeroWhenNoAttendances(): void
    {
        $participant = new BaseParticipant();

        self::assertSame(0.0, $participant->getUnpaidSum());
    }

    public function testGetPaidSumIsZeroWhenNoAttendances(): void
    {
        $participant = new BaseParticipant();

        self::assertSame(0.0, $participant->getPaidSum());
    }

    public function testGetCommentsInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getConversation());
    }

    public function testAddAccessCodeAddsCode(): void
    {
        $participant = new BaseParticipant();
        $strategy = $this->createStub(AccessCodeStrategy::class);
        $code = new AccessCode($strategy, 'ABC123');

        $participant->addAccessCode($code);

        self::assertCount(1, $participant->getAccessCodes());
        self::assertTrue($participant->getAccessCodes()->contains($code));
    }

    public function testAddAccessCodeIgnoresNull(): void
    {
        $participant = new BaseParticipant();

        $participant->addAccessCode(null);

        self::assertCount(0, $participant->getAccessCodes());
    }

    public function testAddAccessCodeIsIdempotent(): void
    {
        $participant = new BaseParticipant();
        $strategy = $this->createStub(AccessCodeStrategy::class);
        $code = new AccessCode($strategy, 'ABC123');

        $participant->addAccessCode($code);
        $participant->addAccessCode($code);

        self::assertCount(1, $participant->getAccessCodes());
    }

    public function testRemoveAccessCode(): void
    {
        $participant = new BaseParticipant();
        $strategy = $this->createStub(AccessCodeStrategy::class);
        $code = new AccessCode($strategy, 'ABC123');

        $participant->addAccessCode($code);
        self::assertCount(1, $participant->getAccessCodes());

        $participant->removeAccessCode($code);

        self::assertCount(0, $participant->getAccessCodes());
    }

    public function testAddFriendCodeAddsFriendCode(): void
    {
        $participant = new BaseParticipant();
        $offer = new Base();
        $friendCode = new FriendCode($offer, 'FRIEND1');

        $participant->addFriendCode($friendCode);

        self::assertCount(1, $participant->getFriendCodes());
    }

    public function testAddFriendCodeIgnoresNull(): void
    {
        $participant = new BaseParticipant();

        $participant->addFriendCode(null);

        self::assertCount(0, $participant->getFriendCodes());
    }

    public function testAddDuplicateAddsEntry(): void
    {
        $participant = new BaseParticipant();
        $duplicate = new BaseParticipant();

        $participant->addDuplicate($duplicate, 'similar');

        self::assertCount(1, $participant->getDuplicates());
    }

    public function testAddDuplicateUpdatesTypeForExistingDuplicate(): void
    {
        $participant = new BaseParticipant();
        $duplicate = new BaseParticipant();

        $participant->addDuplicate($duplicate, 'similar');
        $participant->addDuplicate($duplicate, 'tolerated');

        // Should update rather than add another
        self::assertCount(1, $participant->getDuplicates());
    }

    public function testGetConsentsInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getConsents());
    }

    public function testGetActivityInitiallyEmpty(): void
    {
        $participant = new BaseParticipant();

        self::assertCount(0, $participant->getActivity());
    }

    public function testGetPostalCodeFromPostalAddress(): void
    {
        $participant = new BaseParticipant();
        $address = new PostalAddress();
        $address->setPostalCode('12345');
        $participant->setPostalAddress($address);

        self::assertSame('12345', $participant->getPostalCode());
    }

    public function testGetPostalCodeFromUserPostalAddress(): void
    {
        $postalAddress = new PostalAddress();
        $postalAddress->setPostalCode('99999');

        $user = $this->createStub(User::class);
        $user->method('getPostalAddresses')->willReturn(new \Doctrine\Common\Collections\ArrayCollection([$postalAddress]));

        $participant = new BaseParticipant($user);

        self::assertSame('99999', $participant->getPostalCode());
    }

    public function testGetAttendancesConfirmedWithCancelledFalseExcludesCancelled(): void
    {
        $participant = new BaseParticipant();
        $this->injectAttendance($participant, $this->makeAttendance(AttendanceStatus::CONFIRMED, true));

        self::assertCount(0, $participant->getAttendancesConfirmed(false));
    }

    public function testGetAttendancesConfirmedWithCancelledFalseIncludesNonCancelled(): void
    {
        $participant = new BaseParticipant();
        $this->injectAttendance($participant, $this->makeAttendance(AttendanceStatus::CONFIRMED, false));

        self::assertCount(1, $participant->getAttendancesConfirmed(false));
    }

    public function testGetAttendancesWaitlistedWithCancelledFalse(): void
    {
        $participant = new BaseParticipant();
        $this->injectAttendance($participant, $this->makeAttendance(AttendanceStatus::WAITLISTED, false));

        self::assertCount(1, $participant->getAttendancesWaitlisted(false));
    }

    public function testGetAttendancesWaitingWithCancelledFalse(): void
    {
        $participant = new BaseParticipant();
        $this->injectAttendance($participant, $this->makeAttendance(AttendanceStatus::WAITING, false));

        self::assertCount(1, $participant->getAttendancesWaiting(false));
    }

    public function testGetAttendancesNotWithdrawnWithIncludedCancelledFalse(): void
    {
        $participant = new BaseParticipant();
        $this->injectAttendance($participant, $this->makeAttendance(AttendanceStatus::CONFIRMED, false));

        self::assertCount(1, $participant->getAttendancesNotWithdrawn(null, false));
    }

    public function testGetAttendancesNotHiddenWithIncludedCancelledFalse(): void
    {
        $participant = new BaseParticipant();
        $this->injectAttendance($participant, $this->makeAttendance(AttendanceStatus::CONFIRMED, false));

        self::assertCount(1, $participant->getAttendancesNotHidden(null, false));
    }

    public function testGetMobileReturnsNullWhenNoMobileAndNoUser(): void
    {
        $participant = new BaseParticipant(); // no user, no mobile

        self::assertNull($participant->getMobile());
    }

    public function testAddFriendCodeDoesNotDuplicateAlreadyAddedCode(): void
    {
        $offer = $this->createStub(OfferInterface::class);
        $code = new FriendCode($offer, 'ABC123');

        $participant = new BaseParticipant();
        $participant->addFriendCode($code);
        $participant->addFriendCode($code); // second call should be no-op

        self::assertCount(1, $participant->getFriendCodes());
    }

    public function testGetAttendancesNotWithdrawnIncludesAttendancesFromToleratedDuplicate(): void
    {
        $offer = new Base();

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

        // Add the attendance to the duplicate participant's collection via reflection
        $rp = new \ReflectionProperty($duplicateParticipant, 'attendances');
        $rp->getValue($duplicateParticipant)->add($attendance);

        $participant = new BaseParticipant();
        $participant->addDuplicate($duplicateParticipant, ParticipantDuplicate::TYPE_TOLERATED);

        $result = $participant->getAttendancesNotWithdrawn();

        self::assertCount(1, $result);
    }

    private function injectAttendance(BaseParticipant $participant, Attendance $attendance): void
    {
        $prop = new \ReflectionProperty(BaseParticipant::class, 'attendances');
        $prop->getValue($participant)->add($attendance);
    }

    private function makeAttendance(string $status, bool $offerCancelled = false): Attendance
    {
        $offer = $this->createStub(OfferInterface::class);
        $offer->method('isCancelled')->willReturn($offerCancelled);
        $offer->method('getEdition')->willReturn(null);

        $attendance = new Attendance($offer, null);
        $attendance->setStatus($status);

        return $attendance;
    }
}
