<?php

namespace MintyPHP\Tests\Service\Access;

use MintyPHP\Repository\Access\RoleAssignableRoleRepositoryInterface;
use MintyPHP\Repository\Access\RoleRepositoryInterface;
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
use MintyPHP\Service\Access\AssignableRoleService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use PHPUnit\Framework\TestCase;

class AssignableRoleServiceTest extends TestCase
{
    public function testListReturnsEmptyForInvalidActorId(): void
    {
        $service = $this->makeService();

        $this->assertSame([], $service->listAssignableRoleIdsForActor(0));
        $this->assertSame([], $service->listAssignableRoleIdsForActor(-5));
    }

    public function testListReturnsAllActiveRolesWhenActorHasAssignAllPermission(): void
    {
        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())
            ->method('userHas')
            ->with(42, PermissionService::ROLES_ASSIGN_ALL)
            ->willReturn(true);

        $roleRepository = $this->createMock(RoleRepositoryInterface::class);
        $roleRepository->expects($this->once())
            ->method('listActiveIds')
            ->willReturn([1, 2, 3, 4]);

        $service = $this->makeService(
            permissionService: $permissionService,
            roleRepository: $roleRepository,
        );

        $this->assertSame([1, 2, 3, 4], $service->listAssignableRoleIdsForActor(42));
    }

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

        $userRoleRepository = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepository->expects($this->once())
            ->method('listRoleIdsByUserId')
            ->with(42)
            ->willReturn([]);

        $service = $this->makeService(
            permissionService: $permissionService,
            userRoleRepository: $userRoleRepository,
        );

        $this->assertSame([], $service->listAssignableRoleIdsForActor(42));
    }

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

        $userRoleRepository = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepository->method('listRoleIdsByUserId')->willReturn([10, 11]);

        $assignableRepo = $this->createMock(RoleAssignableRoleRepositoryInterface::class);
        $assignableRepo->expects($this->once())
            ->method('listAssignableRoleIdsByRoleIds')
            ->with([10, 11])
            ->willReturn([20, 21, 22]);

        $service = $this->makeService(
            permissionService: $permissionService,
            userRoleRepository: $userRoleRepository,
            assignableRoleRepository: $assignableRepo,
        );

        $this->assertSame([20, 21, 22], $service->listAssignableRoleIdsForActor(42));
    }

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

        $userRoleRepository = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepository->method('listRoleIdsByUserId')->willReturn([10]);

        $assignableRepo = $this->createMock(RoleAssignableRoleRepositoryInterface::class);
        $assignableRepo->method('listAssignableRoleIdsByRoleIds')->willReturn([20, 21]);

        $service = $this->makeService(
            permissionService: $permissionService,
            userRoleRepository: $userRoleRepository,
            assignableRoleRepository: $assignableRepo,
        );

        // currentIds contains a non-assignable role 99 (frozen) plus assignable 20 (touchable).
        // submittedIds drops 20, adds 21, attempts to add 99 again and a forbidden 55.
        $result = $service->computeEffectiveRoleIds(
            actorUserId: 42,
            submittedIds: [21, 99, 55],
            currentIds: [99, 20],
        );

        // Frozen: 99. Touched: 21. 55 is not assignable so dropped. 20 removed because actor chose not to keep it.
        $this->assertSame([99, 21], $result);
    }

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

        $roleRepository = $this->createMock(RoleRepositoryInterface::class);
        $roleRepository->method('listActiveIds')->willReturn([1, 2, 3]);

        $service = $this->makeService(
            permissionService: $permissionService,
            roleRepository: $roleRepository,
        );

        $result = $service->computeEffectiveRoleIds(42, [1, 999], []);

        $this->assertSame([1], $result);
    }

    public function testReplaceAssignableRolesRecordsAuditOnChange(): void
    {
        $assignableRepo = $this->createMock(RoleAssignableRoleRepositoryInterface::class);
        $assignableRepo->method('listAssignableRoleIdsByRoleId')
            ->willReturnOnConsecutiveCalls([1, 2], [1, 2, 3]);
        $assignableRepo->expects($this->once())
            ->method('replaceForRole')
            ->with(10, [1, 2, 3])
            ->willReturn(true);

        $audit = $this->createMock(AuditRecorderInterface::class);
        $audit->expects($this->once())
            ->method('record')
            ->with(
                'admin.roles.assignable_roles.update',
                'success',
                $this->callback(static function (array $metadata): bool {
                    return ($metadata['actor_user_id'] ?? null) === 42
                        && ($metadata['target_id'] ?? null) === 10
                        && ($metadata['before']['assignable_role_ids'] ?? null) === [1, 2]
                        && ($metadata['after']['assignable_role_ids'] ?? null) === [1, 2, 3];
                })
            );

        $service = $this->makeService(
            assignableRoleRepository: $assignableRepo,
            systemAuditService: $audit,
        );

        $this->assertTrue($service->replaceAssignableRoles(10, [1, 2, 3], 42));
    }

    public function testReplaceAssignableRolesSkipsAuditWhenNothingChanged(): void
    {
        $assignableRepo = $this->createMock(RoleAssignableRoleRepositoryInterface::class);
        $assignableRepo->method('listAssignableRoleIdsByRoleId')
            ->willReturnOnConsecutiveCalls([1, 2], [1, 2]);
        $assignableRepo->method('replaceForRole')->willReturn(true);

        $audit = $this->createMock(AuditRecorderInterface::class);
        $audit->expects($this->never())->method('record');

        $service = $this->makeService(
            assignableRoleRepository: $assignableRepo,
            systemAuditService: $audit,
        );

        $this->assertTrue($service->replaceAssignableRoles(10, [1, 2], 42));
    }

    public function testReplaceAssignableRolesNormalizesNonPositiveActorIdInAudit(): void
    {
        $assignableRepo = $this->createMock(RoleAssignableRoleRepositoryInterface::class);
        $assignableRepo->method('listAssignableRoleIdsByRoleId')
            ->willReturnOnConsecutiveCalls([], [5]);
        $assignableRepo->method('replaceForRole')->willReturn(true);

        $audit = $this->createMock(AuditRecorderInterface::class);
        $audit->expects($this->once())
            ->method('record')
            ->with(
                'admin.roles.assignable_roles.update',
                'success',
                $this->callback(static fn (array $m): bool => array_key_exists('actor_user_id', $m) && $m['actor_user_id'] === null)
            );

        $service = $this->makeService(
            assignableRoleRepository: $assignableRepo,
            systemAuditService: $audit,
        );

        $service->replaceAssignableRoles(10, [5], 0);
    }

    private function makeService(
        ?RoleAssignableRoleRepositoryInterface $assignableRoleRepository = null,
        ?UserRoleRepositoryInterface $userRoleRepository = null,
        ?PermissionService $permissionService = null,
        ?RoleRepositoryInterface $roleRepository = null,
        ?AuditRecorderInterface $systemAuditService = null,
    ): AssignableRoleService {
        return new AssignableRoleService(
            $assignableRoleRepository ?? $this->createMock(RoleAssignableRoleRepositoryInterface::class),
            $userRoleRepository ?? $this->createMock(UserRoleRepositoryInterface::class),
            $permissionService ?? $this->createMock(PermissionService::class),
            $roleRepository ?? $this->createMock(RoleRepositoryInterface::class),
            $systemAuditService ?? $this->createMock(AuditRecorderInterface::class),
        );
    }
}
