<?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\ConversationItem;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\PostalAddress;
use Ferienpass\CoreBundle\Entity\User;
use PHPUnit\Framework\TestCase;

final class UserTest extends TestCase
{
    public function testGetNameCombinesFirstAndLast(): void
    {
        $user = new User();
        $user->setFirstname('John');
        $user->setLastname('Doe');

        self::assertSame('John Doe', $user->getName());
    }

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

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

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

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

    public function testGetRolesAlwaysContainsRoleUser(): void
    {
        $user = new User();

        self::assertContains('ROLE_USER', $user->getRoles());
    }

    public function testSetRoles(): void
    {
        $user = new User();
        $user->setRoles(['ROLE_ADMIN']);

        self::assertContains('ROLE_ADMIN', $user->getRoles());
        self::assertContains('ROLE_USER', $user->getRoles());
    }

    public function testIsAdminFalseByDefault(): void
    {
        $user = new User();

        self::assertFalse($user->isAdmin());
    }

    public function testIsAdminTrueWhenRoleSet(): void
    {
        $user = new User();
        $user->setRoles(['ROLE_ADMIN']);

        self::assertTrue($user->isAdmin());
    }

    public function testIsSuperAdminFalseByDefault(): void
    {
        $user = new User();

        self::assertFalse($user->isSuperAdmin());
    }

    public function testIsSuperAdminTrueWhenRoleSet(): void
    {
        $user = new User();
        $user->setRoles(['ROLE_SUPER_ADMIN']);

        self::assertTrue($user->isSuperAdmin());
    }

    public function testAddAdminRole(): void
    {
        $user = new User();
        $user->addAdminRole('ROLE_CMS_ADMIN');

        self::assertContains('ROLE_CMS_ADMIN', $user->getRoles());
        self::assertContains('ROLE_CMS_ADMIN', $user->getAdminRoles());
    }

    public function testAddAdminRoleIgnoresUnknownRoles(): void
    {
        $user = new User();
        $user->addAdminRole('ROLE_UNKNOWN');

        self::assertNotContains('ROLE_UNKNOWN', $user->getRoles());
    }

    public function testAddAdminRoleIsIdempotent(): void
    {
        $user = new User();
        $user->addAdminRole('ROLE_CMS_ADMIN');
        $user->addAdminRole('ROLE_CMS_ADMIN'); // second add should be ignored

        self::assertCount(1, array_filter($user->getAdminRoles(), static fn ($r) => 'ROLE_CMS_ADMIN' === $r));
    }

    public function testRemoveAdminRole(): void
    {
        $user = new User();
        $user->addAdminRole('ROLE_CMS_ADMIN');
        $user->removeAdminRole('ROLE_CMS_ADMIN');

        self::assertNotContains('ROLE_CMS_ADMIN', $user->getRoles());
    }

    public function testRemoveAdminRoleIgnoresNonAdminRoles(): void
    {
        $user = new User();
        $user->setRoles(['ROLE_ADMIN']);
        $user->removeAdminRole('ROLE_ADMIN'); // ROLE_ADMIN is not in ADMIN_ROLES constant

        self::assertContains('ROLE_ADMIN', $user->getRoles()); // unchanged
    }

    public function testAddAccountRole(): void
    {
        $user = new User();
        $user->addAccountRole('ROLE_HOST');

        self::assertContains('ROLE_HOST', $user->getRoles());
        self::assertContains('ROLE_HOST', $user->getAccountRoles());
    }

    public function testAddAccountRoleIgnoresUnknownRoles(): void
    {
        $user = new User();
        $user->addAccountRole('ROLE_CUSTOM');

        self::assertNotContains('ROLE_CUSTOM', $user->getRoles());
    }

    public function testRemoveAccountRole(): void
    {
        $user = new User();
        $user->addAccountRole('ROLE_HOST');
        $user->removeAccountRole('ROLE_HOST');

        self::assertNotContains('ROLE_HOST', $user->getRoles());
    }

    public function testIsDisabledFalseByDefault(): void
    {
        $user = new User();

        self::assertFalse($user->isDisabled());
    }

    public function testSetDisabled(): void
    {
        $user = new User();
        $user->setDisabled(true);

        self::assertTrue($user->isDisabled());
    }

    public function testIsNotActivatedBeforeBeingEnabled(): void
    {
        $user = new User();
        $user->setDisabled(true);

        self::assertNull($user->getActivatedAt());
        self::assertFalse($user->isActivated());
    }

    public function testEnablingStampsActivatedAt(): void
    {
        // Both the activation link and the admin's "deaktiviert" checkbox end
        // up here — either one activates the account.
        $user = new User();
        $user->setDisabled(true);
        $user->setDisabled(false);

        self::assertTrue($user->isActivated());
        self::assertInstanceOf(\DateTimeImmutable::class, $user->getActivatedAt());
    }

    public function testReDisablingAnActivatedAccountKeepsTheStamp(): void
    {
        // The regression: an admin suspending an account it had activated by
        // hand must not make it look like an abandoned registration, or
        // TruncateUnactivatedUsers deletes it (with its participants) after
        // four days.
        $user = new User();
        $user->setDisabled(true);
        $user->setDisabled(false);

        $activatedAt = $user->getActivatedAt();

        $user->setDisabled(true);

        self::assertTrue($user->isActivated());
        self::assertSame($activatedAt, $user->getActivatedAt());
    }

    public function testReEnablingDoesNotMoveTheStamp(): void
    {
        $user = new User();
        $user->setDisabled(true);
        $user->setDisabled(false);

        $activatedAt = $user->getActivatedAt();

        $user->setDisabled(true);
        $user->setDisabled(false);

        self::assertSame($activatedAt, $user->getActivatedAt());
    }

    public function testAccountsBornEnabledAreStampedOnPersist(): void
    {
        // Admin-created accounts, host registrations and accepted invitations
        // never pass through setDisabled(false) — the PrePersist hook is what
        // keeps them out of the cron's reach once an admin suspends them.
        $user = new User();

        self::assertNull($user->getActivatedAt());

        $user->stampActivatedAtOnCreate();

        self::assertTrue($user->isActivated());
    }

    public function testAccountsBornDisabledAreNotStampedOnPersist(): void
    {
        $user = new User();
        $user->setDisabled(true);

        $user->stampActivatedAtOnCreate();

        self::assertNull($user->getActivatedAt());
    }

    public function testIsBlockedEmailFalseByDefault(): void
    {
        $user = new User();

        self::assertFalse($user->isBlockedEmail());
    }

    public function testSetBlockedEmail(): void
    {
        $user = new User();
        $user->setBlockedEmail(true);

        self::assertTrue($user->isBlockedEmail());
    }

    public function testSetPassword(): void
    {
        $user = new User();
        $user->setPassword('hashed_password');

        self::assertSame('hashed_password', $user->getPassword());
    }

    public function testSetPlainPassword(): void
    {
        $user = new User();
        $user->setPlainPassword('plain_secret');

        self::assertSame('plain_secret', $user->getPlainPassword());
    }

    public function testSetLocale(): void
    {
        $user = new User();
        $user->setLocale('de');

        self::assertSame('de', $user->getLocale());
    }

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

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

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

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

    public function testUuidIsGeneratedOnConstruct(): void
    {
        $u1 = new User();
        $u2 = new User();

        self::assertNotSame((string) $u1->getUuid(), (string) $u2->getUuid());
    }

    public function testCreatedAtIsSet(): void
    {
        $before = new \DateTimeImmutable();
        $user = new User();
        $after = new \DateTimeImmutable();

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

    public function testIsHostFalseByDefault(): void
    {
        $user = new User();

        self::assertFalse($user->isHost());
    }

    public function testIsHostTrueWhenRoleSet(): void
    {
        $user = new User();
        $user->setRoles(['ROLE_HOST']);

        self::assertTrue($user->isHost());
    }

    public function testIsMemberFalseByDefault(): void
    {
        $user = new User();

        self::assertFalse($user->isMember());
    }

    public function testIsMemberTrueWhenRoleSet(): void
    {
        $user = new User();
        $user->setRoles(['ROLE_MEMBER']);

        self::assertTrue($user->isMember());
    }

    public function testSetSuperAdminTrue(): void
    {
        $user = new User();
        $user->setSuperAdmin(true);

        self::assertTrue($user->isSuperAdmin());
    }

    public function testSetSuperAdminTrueIsIdempotent(): void
    {
        $user = new User();
        $user->setSuperAdmin(true);
        $user->setSuperAdmin(true);

        self::assertCount(1, array_filter($user->getRoles(), static fn ($r) => 'ROLE_SUPER_ADMIN' === $r));
    }

    public function testSetSuperAdminFalseRemovesRoleAndKeepsAdmin(): void
    {
        $user = new User();
        $user->setSuperAdmin(true);
        $user->setSuperAdmin(false);

        self::assertFalse($user->isSuperAdmin());
        self::assertContains('ROLE_ADMIN', $user->getRoles());
    }

    public function testSetSuperAdminFalseIsNoopWhenNotSet(): void
    {
        $user = new User();
        $user->setSuperAdmin(false);

        self::assertFalse($user->isSuperAdmin());
    }

    public function testEraseCredentialsClearsPlainPassword(): void
    {
        $user = new User();
        $user->setPlainPassword('secret');
        $user->eraseCredentials();

        self::assertNull($user->getPlainPassword());
    }

    public function testSetModifiedAt(): void
    {
        $user = new User();
        $date = new \DateTimeImmutable('2024-01-15 12:00:00');
        $user->setModifiedAt($date);

        self::assertSame($date, $user->getModifiedAt());
    }

    public function testGetModifiedAtIsSetOnConstruct(): void
    {
        $before = new \DateTimeImmutable();
        $user = new User();
        $after = new \DateTimeImmutable();

        self::assertGreaterThanOrEqual($before, $user->getModifiedAt());
        self::assertLessThanOrEqual($after, $user->getModifiedAt());
    }

    public function testSetLastLogin(): void
    {
        $user = new User();
        $date = new \DateTimeImmutable('2024-06-01');
        $user->setLastLogin($date);

        self::assertSame($date, $user->getLastLogin());
    }

    public function testGetLastLoginNullByDefault(): void
    {
        $user = new User();

        self::assertNull($user->getLastLogin());
    }

    public function testSetPublicFields(): void
    {
        $user = new User();
        $user->setPublicFields(['email', 'phone']);

        self::assertSame(['email', 'phone'], $user->getPublicFields());
    }

    public function testSetDontDeleteBefore(): void
    {
        $user = new User();
        $date = new \DateTimeImmutable('2025-12-31');
        $user->setDontDeleteBefore($date);

        self::assertSame($date, $user->getDontDeleteBefore());
    }

    public function testGetDontDeleteBeforeNullByDefault(): void
    {
        $user = new User();

        self::assertNull($user->getDontDeleteBefore());
    }

    public function testMarkAsTokenAuthenticatedAddsScopes(): void
    {
        $user = new User();
        $user->markAsTokenAuthenticated(['SCOPE_READ', 'SCOPE_WRITE']);

        self::assertContains('SCOPE_READ', $user->getRoles());
        self::assertContains('SCOPE_WRITE', $user->getRoles());
    }

    public function testAddPostalAddress(): void
    {
        $user = new User();
        $address = new PostalAddress();
        $user->addPostalAddress($address);

        self::assertCount(1, $user->getPostalAddresses());
    }

    public function testAddPostalAddressIgnoresNull(): void
    {
        $user = new User();
        $user->addPostalAddress(null);

        self::assertCount(0, $user->getPostalAddresses());
    }

    public function testRemovePostalAddress(): void
    {
        $user = new User();
        $address = new PostalAddress();
        $user->addPostalAddress($address);
        $user->removePostalAddress($address);

        self::assertCount(0, $user->getPostalAddresses());
    }

    public function testPostalAddressesInitiallyEmpty(): void
    {
        $user = new User();

        self::assertCount(0, $user->getPostalAddresses());
    }

    public function testGetAccountRoles(): void
    {
        $user = new User();
        $user->addAccountRole('ROLE_HOST');
        $user->addAccountRole('ROLE_MEMBER');

        $accountRoles = $user->getAccountRoles();
        self::assertContains('ROLE_HOST', $accountRoles);
        self::assertContains('ROLE_MEMBER', $accountRoles);
    }

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

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

    public function testGetLastname(): void
    {
        $user = new User();
        $user->setLastname('Wonder');

        self::assertSame('Wonder', $user->getLastname());
    }

    public function testCreateDebtor(): void
    {
        $user = new User();
        $user->createDebtor();

        self::assertInstanceOf(\Ferienpass\CoreBundle\Entity\Debtor::class, $user->getDebtor());
    }

    public function testCreateDebtorIsIdempotent(): void
    {
        $user = new User();
        $user->createDebtor();
        $debtor1 = $user->getDebtor();
        $user->createDebtor();
        $debtor2 = $user->getDebtor();

        self::assertSame($debtor1, $debtor2);
    }

    public function testGetDebtorCreatesWhenNotSet(): void
    {
        $user = new User();

        self::assertInstanceOf(\Ferienpass\CoreBundle\Entity\Debtor::class, $user->getDebtor());
    }

    public function testConsentsInitiallyEmpty(): void
    {
        $user = new User();

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

    public function testParticipantsInitiallyEmpty(): void
    {
        $user = new User();

        self::assertCount(0, $user->getParticipants());
    }

    public function testPaymentsInitiallyEmpty(): void
    {
        $user = new User();

        self::assertCount(0, $user->getPayments());
    }

    public function testActivityInitiallyEmpty(): void
    {
        $user = new User();

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

    public function testApiTokensInitiallyEmpty(): void
    {
        $user = new User();

        self::assertCount(0, $user->getApiTokens());
    }

    public function testGetBlockedContactNullByDefault(): void
    {
        $user = new User();

        self::assertNull($user->getBlockedContact());
    }

    public function testGetHostsInitiallyEmpty(): void
    {
        $user = new User();

        self::assertCount(0, $user->getHosts());
    }

    public function testHasHostReturnsFalseInitially(): void
    {
        $user = new User();
        $host = new Host();

        self::assertFalse($user->hasHost($host));
    }

    public function testAddHostMakesHasHostTrue(): void
    {
        $user = new User();
        $host = new Host();

        $user->addHost($host);

        self::assertTrue($user->hasHost($host));
        self::assertCount(1, $user->getHosts());
    }

    public function testAddHostIsIdempotent(): void
    {
        $user = new User();
        $host = new Host();

        $user->addHost($host);
        $user->addHost($host);

        self::assertCount(1, $user->getHosts());
    }

    public function testRemoveHostMakesHasHostFalse(): void
    {
        $user = new User();
        $host = new Host();

        $user->addHost($host);
        self::assertTrue($user->hasHost($host));

        $user->removeHost($host);

        self::assertFalse($user->hasHost($host));
    }

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

        $user->addParticipant($participant);

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

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

        $user->addParticipant($participant);
        self::assertCount(1, $user->getParticipants());

        $user->removeParticipant($participant);

        self::assertCount(0, $user->getParticipants());
    }

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

        // Should not throw
        $user->removeParticipant($participant);

        self::assertCount(0, $user->getParticipants());
    }

    public function testCommentsInitiallyEmpty(): void
    {
        $user = new User();

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

    public function testGetConversationReturnsConversationItems(): void
    {
        $user = new User();

        $user->getConversation()->add(new ConversationItem($user, null, 'Some comment'));

        self::assertCount(1, $user->getConversation());
    }

    public function testRemoveAccountRoleDoesNotThrowWhenRoleNotPresent(): void
    {
        $user = new User();
        // ROLE_HOST is a valid account role, but user doesn't have it → early return
        $user->removeAccountRole('ROLE_HOST');

        self::assertNotContains('ROLE_HOST', $user->getRoles());
    }

    public function testRemoveAccountRoleDoesNotThrowWhenRoleNotInAccountRoles(): void
    {
        $user = new User();
        // ROLE_SOMETHING is not a valid account role → early return
        $user->removeAccountRole('ROLE_SOMETHING_INVALID');

        self::assertNotContains('ROLE_SOMETHING_INVALID', $user->getRoles());
    }

    public function testRemovePostalAddressDoesNothingWhenNotPresent(): void
    {
        $user = new User();
        $address = new PostalAddress();

        // Address was never added — should not throw
        $user->removePostalAddress($address);

        self::assertCount(0, $user->getPostalAddresses());
    }
}
