<?php declare(strict_types=1);

namespace Apexl\Tests\User\FeatureTests;

use apexl\Io\modules\user\entities\roleEntity;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\enums\permissions\Common;
use apexl\Io\TestInfrastructure\FeatureTest;
use Apexl\Tests\User\Helpers\TestPermissionsEnum;
use PHPUnit\Framework\Attributes\Test;

final class PermissionsTest extends FeatureTest
{

    #[Test]
    public function managePermissionsDirectly(): void
    {
        $user = userEntity::factory()->create();
        $user->addPermission(TestPermissionsEnum::HAS_PERMISSION);

        $this->assertTrue($user->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
        $this->assertFalse($user->isAllowed(TestPermissionsEnum::DOES_NOT_HAVE_PERMISSION));

        $secondUser = userEntity::factory()->create();

        $this->assertFalse($secondUser->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
        $this->assertFalse($secondUser->isAllowed(TestPermissionsEnum::DOES_NOT_HAVE_PERMISSION));

        $user->removePermission(TestPermissionsEnum::HAS_PERMISSION);

        $this->assertFalse($user->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
    }

    #[Test]
    public function managePermissionsViaRole(): void
    {
        $user = userEntity::factory()->create();
        $role = roleEntity::factory()->create();

        $role->addPermission(TestPermissionsEnum::HAS_PERMISSION);
        $user->addRole($role);

        $this->assertTrue($user->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
        $this->assertFalse($user->isAllowed(TestPermissionsEnum::DOES_NOT_HAVE_PERMISSION));

        $secondUser = userEntity::factory()->create();

        $this->assertFalse($secondUser->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
        $this->assertFalse($secondUser->isAllowed(TestPermissionsEnum::DOES_NOT_HAVE_PERMISSION));

        $user->removeRole($role);

        $this->assertFalse($user->isAllowed(TestPermissionsEnum::HAS_PERMISSION));

        $role->removePermission(TestPermissionsEnum::HAS_PERMISSION);

        $user->addRole($role);
        $this->assertFalse($user->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
    }

    #[Test]
    public function common(): void
    {
        $user = userEntity::factory()->create();

        $this->assertTrue($user->isAllowed(Common::ALLOW_ALL));
        $this->assertFalse($user->isAllowed(Common::IS_LOGGED_IN));
        $this->assertFalse($user->isAllowed(Common::CAN_DO_NOTHING));
    }

    #[Test]
    public function bypass(): void
    {
        $user = userEntity::factory()->create();
        $role = roleEntity::factory()->create();
        $role->addPermission(Common::BYPASS);
        $user->addRole($role);

        $this->assertTrue($user->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
        $this->assertTrue($user->isAllowed(TestPermissionsEnum::DOES_NOT_HAVE_PERMISSION));
        $this->assertFalse($user->isAllowed(Common::CAN_DO_NOTHING));
    }

    /**
     * Anonymous/guest user (null id — the shape PermissionMiddleware
     * constructs via `$this->currentUserFactory->get() ?? new userEntity()`)
     * must resolve permission-gated checks to false instead of throwing.
     * Previously `UserPermissionManagerService::permissions()` passed the
     * null id straight into `UserPermissionOperator::listForUser(int $userId)`,
     * a TypeError that wasn't an ExecutionException so it skipped the
     * try/catch's PermissionException wrapping entirely and surfaced as an
     * uncaught 500 for every anonymous request to a gated route.
     */
    #[Test]
    public function anonymousUserIsNeverAllowedAGatedPermission(): void
    {
        $anonymous = new userEntity();

        $this->assertNull($anonymous->id);
        $this->assertFalse($anonymous->isAllowed(TestPermissionsEnum::HAS_PERMISSION));
        $this->assertFalse($anonymous->isAllowed(TestPermissionsEnum::DOES_NOT_HAVE_PERMISSION));
        $this->assertFalse($anonymous->isAllowed(Common::CAN_DO_NOTHING));

        // ALLOW_ALL must stay reachable for anonymous visitors regardless
        // (installer / public routes rely on this short-circuiting before
        // ever touching UserPermissionManagerService).
        $this->assertTrue($anonymous->isAllowed(Common::ALLOW_ALL));

        // permissions() itself must resolve to an empty set rather than
        // throwing, both with and without role lookup.
        $this->assertCount(0, $anonymous->permissions());
        $this->assertCount(0, $anonymous->permissions(includeRoles: false));
    }

    /**
     * Same latent bug as anonymousUserIsNeverAllowedAGatedPermission() above,
     * but reached directly via userEntity::roles()/hasRole() rather than
     * through the permission-manager's $includeRoles branch:
     * UserRoleManagerService::roles() passed the anonymous user's null id
     * straight into UserRoleOperator::listForUser(int $userId) unguarded,
     * throwing a TypeError instead of resolving to an empty roleCollection.
     * Guarded the same way as UserPermissionManagerService::permissions()
     * (commit 4a4677c).
     */
    #[Test]
    public function anonymousUserRolesResolvesEmptyInsteadOfThrowing(): void
    {
        $anonymous = new userEntity();
        // A real, existing role name so hasRole() below resolves the role id
        // and actually reaches UserRoleManagerService::roles() (via has()'s
        // $this->roles()->exists(...)) rather than short-circuiting on
        // RecordNotFoundException for an unknown name.
        $role = roleEntity::factory()->create();

        $this->assertNull($anonymous->id);
        $this->assertCount(0, $anonymous->roles());
        $this->assertFalse($anonymous->hasRole($role->name));
    }
}
