<?php

namespace MintyPHP\Tests\Service\Access;

use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\Tenant\TenantScopeService;
use PHPUnit\Framework\TestCase;

class UserAuthorizationPolicyTest extends TestCase
{
    use AuthorizationPolicyTestSupport;

    public function testAdminViewRequiresUsersViewPermission(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())
            ->method('userHas')
            ->with(13, PermissionService::USERS_VIEW)
            ->willReturn(false);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW, [
            'actor_user_id' => 13,
        ]);

        $this->assertForbiddenDecision($decision);
    }

    public function testAdminCreateRequiresUsersCreatePermission(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())
            ->method('userHas')
            ->with(13, PermissionService::USERS_CREATE)
            ->willReturn(false);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE, [
            'actor_user_id' => 13,
        ]);

        $this->assertDeniedDecision($decision, 403, 'forbidden');
    }

    public function testAdminActivateRequiresUsersUpdatePermission(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())
            ->method('userHas')
            ->with(11, PermissionService::USERS_UPDATE)
            ->willReturn(false);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->never())->method('canAccess');

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE, [
            'actor_user_id' => 11,
            'target_user_id' => 22,
        ]);

        $this->assertForbiddenDecision($decision);
    }

    public function testAdminActivateReturnsPermissionDeniedWhenTenantScopeFails(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            11 => [PermissionService::USERS_UPDATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 22, 11)
            ->willReturn(false);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE, [
            'actor_user_id' => 11,
            'target_user_id' => 22,
        ]);

        $this->assertDeniedDecision($decision, 403, 'permission_denied');
    }

    public function testAdminDeactivateAllowsSelfWithoutScopeLookup(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            9 => [PermissionService::USERS_UPDATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->never())->method('canAccess');

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DEACTIVATE, [
            'actor_user_id' => 9,
            'target_user_id' => 9,
        ]);

        $this->assertTrue($decision->isAllowed());
    }

    public function testAdminBulkDeleteRequiresUsersDeletePermission(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())
            ->method('userHas')
            ->with(7, PermissionService::USERS_DELETE)
            ->willReturn(false);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);

        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_BULK, [
            'actor_user_id' => 7,
            'bulk_action' => 'delete',
        ]);

        $this->assertDeniedDecision($decision, 403, 'forbidden');
    }

    public function testAdminDeleteReturnsPermissionDeniedWhenTenantScopeFails(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            7 => [PermissionService::USERS_DELETE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 17, 7)
            ->willReturn(false);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DELETE, [
            'actor_user_id' => 7,
            'target_user_id' => 17,
        ]);

        $this->assertDeniedDecision($decision, 403, 'permission_denied');
    }

    public function testAdminAccessPdfBulkRequiresPermission(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())
            ->method('userHas')
            ->with(21, PermissionService::USERS_ACCESS_PDF)
            ->willReturn(false);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->never())->method('canAccess');

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK, [
            'actor_user_id' => 21,
        ]);

        $this->assertDeniedDecision($decision, 403, 'forbidden');
    }

    public function testAdminApiTokensManageRequiresScopeForOtherUsers(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            8 => [PermissionService::API_TOKENS_MANAGE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 18, 8)
            ->willReturn(false);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE, [
            'actor_user_id' => 8,
            'target_user_id' => 18,
        ]);

        $this->assertDeniedDecision($decision, 403, 'permission_denied');
    }

    public function testAdminSendAccessAllowsSelfWithSelfUpdatePermission(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            30 => [PermissionService::USERS_SELF_UPDATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->never())->method('canAccess');

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_SEND_ACCESS, [
            'actor_user_id' => 30,
            'target_user_id' => 30,
        ]);

        $this->assertTrue($decision->isAllowed());
    }

    public function testEditContextAllowsOwnAccountWithUsersSelfUpdate(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            30 => [PermissionService::USERS_SELF_UPDATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->never())->method('canAccess');
        $scopeGateway->expects($this->once())
            ->method('getUserTenantIds')
            ->with(30)
            ->willReturn([2, 3]);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
            'actor_user_id' => 30,
            'target_user_id' => 30,
        ]);

        $capabilities = $decision->attribute('capabilities', []);
        $this->assertTrue($decision->isAllowed());
        $this->assertTrue($capabilities['is_own_account'] ?? false);
        $this->assertTrue($capabilities['can_edit_user'] ?? false);
        $this->assertTrue($capabilities['can_view_page'] ?? false);
        $this->assertTrue($capabilities['can_view_security_artifacts'] ?? false);
        $this->assertSame([2, 3], $capabilities['allowed_tenant_ids'] ?? []);
    }

    public function testEditContextReturnsPermissionDeniedWhenTargetIsOutOfScope(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 66, 33)
            ->willReturn(false);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
            'actor_user_id' => 33,
            'target_user_id' => 66,
        ]);

        $this->assertDeniedDecision($decision, 403, 'permission_denied');
    }

    public function testEditContextHidesSecurityArtifactsWithoutAuditPermission(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            9 => [PermissionService::USERS_VIEW],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 55, 9)
            ->willReturn(true);
        $scopeGateway->expects($this->once())
            ->method('getUserTenantIds')
            ->with(9)
            ->willReturn([1]);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
            'actor_user_id' => 9,
            'target_user_id' => 55,
        ]);

        $this->assertTrue($decision->isAllowed());
        $this->assertFalse((bool) ($decision->attribute('capabilities', [])['can_view_security_artifacts'] ?? true));
    }

    public function testEditContextShowsSecurityArtifactsWithAuditPermission(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            9 => [PermissionService::USERS_VIEW, PermissionService::USERS_VIEW_AUDIT],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 55, 9)
            ->willReturn(true);
        $scopeGateway->expects($this->once())
            ->method('getUserTenantIds')
            ->with(9)
            ->willReturn([1]);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
            'actor_user_id' => 9,
            'target_user_id' => 55,
        ]);

        $this->assertTrue($decision->isAllowed());
        $this->assertTrue((bool) ($decision->attribute('capabilities', [])['can_view_security_artifacts'] ?? false));
    }

    public function testEditSubmitRejectsUserWithoutEditCapability(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            9 => [PermissionService::USERS_VIEW],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 55, 9)
            ->willReturn(true);
        $scopeGateway->expects($this->once())
            ->method('getUserTenantIds')
            ->with(9)
            ->willReturn([1]);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT, [
            'actor_user_id' => 9,
            'target_user_id' => 55,
        ]);

        $this->assertForbiddenDecision($decision);
    }

    public function testAvatarUploadAllowsOwnAccountWithSelfUpdate(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            19 => [PermissionService::USERS_SELF_UPDATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->never())->method('canAccess');

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_UPLOAD, [
            'actor_user_id' => 19,
            'target_user_id' => 19,
        ]);

        $this->assertTrue($decision->isAllowed());
    }

    public function testAvatarDeleteReturnsPermissionDeniedOutOfScope(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 42, 7)
            ->willReturn(false);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_DELETE, [
            'actor_user_id' => 7,
            'target_user_id' => 42,
        ]);

        $this->assertDeniedDecision($decision, 403, 'permission_denied');
    }

    public function testAvatarViewAllowsInScopeAddressBookViewer(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            5 => [PermissionService::ADDRESS_BOOK_VIEW],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 70, 5)
            ->willReturn(true);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW, [
            'actor_user_id' => 5,
            'target_user_id' => 70,
        ]);

        $this->assertTrue($decision->isAllowed());
    }

    public function testAvatarViewRejectsInScopeUserWithoutAnyRelevantPermission(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->method('userHas')->willReturn(false);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 70, 5)
            ->willReturn(true);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW, [
            'actor_user_id' => 5,
            'target_user_id' => 70,
        ]);

        $this->assertDeniedDecision($decision, 403, 'forbidden');
    }

    public function testApiShowReturnsNotFoundWhenTenantScopeFails(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            4 => [PermissionService::USERS_VIEW],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 44, 4)
            ->willReturn(false);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET, [
            'actor_user_id' => 4,
            'target_user_id' => 44,
            'scoped_tenant_id' => null,
        ]);

        $this->assertDeniedDecision($decision, 404, 'not_found');
    }

    public function testApiShowReturnsNotFoundForScopedTokenCrossTenantResource(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            4 => [PermissionService::USERS_VIEW],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 44, 4)
            ->willReturn(true);
        $scopeGateway->expects($this->once())
            ->method('resourceBelongsToTenant')
            ->with('users', 44, 2)
            ->willReturn(false);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET, [
            'actor_user_id' => 4,
            'target_user_id' => 44,
            'scoped_tenant_id' => 2,
        ]);

        $this->assertDeniedDecision($decision, 404, 'not_found');
    }

    public function testApiCreateForScopedTokenRejectsOutOfScopeTenantIds(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            5 => [PermissionService::USERS_CREATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);

        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_POST, [
            'actor_user_id' => 5,
            'scoped_tenant_id' => 9,
            'input' => ['tenant_ids' => [9, 10]],
        ]);

        $this->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden');
    }

    public function testApiCreateForScopedTokenEnforcesScopedTenantInInput(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            5 => [PermissionService::USERS_CREATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);

        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_POST, [
            'actor_user_id' => 5,
            'scoped_tenant_id' => 9,
            'input' => ['first_name' => 'Ada', 'tenant_ids' => [9]],
        ]);

        $this->assertTrue($decision->isAllowed());
        $this->assertSame([9], $decision->attribute('input')['tenant_ids'] ?? []);
        $this->assertSame(9, $decision->attribute('input')['primary_tenant_id'] ?? null);
    }

    public function testApiUpdateForScopedTokenRejectsOutOfScopePrimaryTenant(): void
    {
        $permissionService = $this->permissionGatewayAllowing([
            6 => [PermissionService::USERS_UPDATE],
        ]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('canAccess')
            ->with('users', 42, 6)
            ->willReturn(true);
        $scopeGateway->expects($this->once())
            ->method('resourceBelongsToTenant')
            ->with('users', 42, 2)
            ->willReturn(true);

        $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway);
        $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_UPDATE, [
            'actor_user_id' => 6,
            'target_user_id' => 42,
            'scoped_tenant_id' => 2,
            'input' => ['primary_tenant_id' => 7],
        ]);

        $this->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden');
    }
}
