<?php

namespace MintyPHP\Tests\Service\User;

use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
use MintyPHP\Repository\Org\UserDepartmentRepositoryInterface;
use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Access\AssignableRoleService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\User\UserAssignmentService;
use MintyPHP\Service\User\UserDirectoryGateway;
use PHPUnit\Framework\TestCase;

class UserAssignmentServiceTest extends TestCase
{
    // ── buildAssignmentsForUser ──────────────────────────────────────────

    public function testBuildAssignmentsForUserReturnsEmptyStructureWhenUserIdIsZero(): void
    {
        $service = $this->newService();
        $result = $service->buildAssignmentsForUser(0);

        $this->assertSame([], $result['tenants']);
        $this->assertSame([], $result['departments']);
        $this->assertSame([], $result['roles']);
    }

    public function testBuildAssignmentsForUserReturnsEmptyStructureWhenUserIdIsNegative(): void
    {
        $service = $this->newService();
        $result = $service->buildAssignmentsForUser(-5);

        $this->assertSame([], $result['tenants']);
        $this->assertSame([], $result['departments']);
        $this->assertSame([], $result['roles']);
    }

    public function testBuildAssignmentsForUserReturnsTenantsDepartmentsAndRoles(): void
    {
        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->expects($this->once())->method('listTenantIdsByUserId')->with(10)->willReturn([1, 2]);

        $userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
        $userDeptRepo->expects($this->once())->method('listDepartmentIdsByUserId')->with(10)->willReturn([100]);

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

        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->once())->method('listTenantsByIds')->with([1, 2])->willReturn([
            ['id' => 1, 'uuid' => 'tenant-1', 'description' => 'Tenant 1', 'status' => 'active'],
            ['id' => 2, 'uuid' => 'tenant-2', 'description' => 'Tenant 2', 'status' => 'active'],
        ]);
        $directoryGateway->expects($this->once())->method('listDepartmentsByIds')->with([100], true)->willReturn([
            ['id' => 100, 'uuid' => 'dept-100', 'description' => 'Dept 100', 'active' => 1, 'tenant_id' => 1],
        ]);
        $directoryGateway->expects($this->once())->method('listRolesByIds')->with([50])->willReturn([
            ['id' => 50, 'uuid' => 'role-50', 'description' => 'Role 50', 'active' => 1],
        ]);

        $service = $this->newService(
            userTenantRepository: $userTenantRepo,
            userDepartmentRepository: $userDeptRepo,
            userRoleRepository: $userRoleRepo,
            directoryGateway: $directoryGateway
        );

        $result = $service->buildAssignmentsForUser(10);

        $this->assertCount(2, $result['tenants']);
        $this->assertSame(1, $result['tenants'][0]['id']);
        $this->assertSame(2, $result['tenants'][1]['id']);
        $this->assertCount(1, $result['departments']);
        $this->assertSame(100, $result['departments'][0]['id']);
        $this->assertSame('tenant-1', $result['departments'][0]['tenant_uuid']);
        $this->assertCount(1, $result['roles']);
        $this->assertSame(50, $result['roles'][0]['id']);
    }

    // ── syncTenants ─────────────────────────────────────────────────────

    public function testSyncTenantsNormalizesAndCallsRepository(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->once())->method('listTenantIds')->willReturn([1, 2, 3]);

        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->expects($this->once())
            ->method('replaceForUser')
            ->with(10, $this->callback(static fn (array $ids): bool => $ids === [1, 2]))
            ->willReturn(true);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);

        $service = $this->newService(
            userWriteRepository: $userWriteRepo,
            userTenantRepository: $userTenantRepo,
            directoryGateway: $directoryGateway
        );

        $result = $service->syncTenants(10, [1, 2]);
        $this->assertTrue($result);
    }

    public function testSyncTenantsDoesNotBumpAuthzWhenFlagIsFalse(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listTenantIds')->willReturn([1]);

        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->method('replaceForUser')->willReturn(true);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->never())->method('bumpAuthzVersion');

        $service = $this->newService(
            userWriteRepository: $userWriteRepo,
            userTenantRepository: $userTenantRepo,
            directoryGateway: $directoryGateway
        );

        $result = $service->syncTenants(10, [1], false);
        $this->assertTrue($result);
    }

    // ── syncRoles ───────────────────────────────────────────────────────

    public function testSyncRolesDeduplicatesAndFiltersAgainstActiveRoles(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->once())->method('listActiveRoleIds')->willReturn([1, 2, 3]);

        $userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepo->expects($this->once())
            ->method('replaceForUser')
            ->with(10, $this->callback(static fn (array $ids): bool => $ids === [1, 3]))
            ->willReturn(true);

        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())->method('clearUserCache')->with(10);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);

        $service = $this->newService(
            userWriteRepository: $userWriteRepo,
            userRoleRepository: $userRoleRepo,
            directoryGateway: $directoryGateway,
            permissionService: $permissionService
        );

        // Pass duplicates and an inactive role (99)
        $result = $service->syncRoles(10, [1, 3, 3, 99]);
        $this->assertTrue($result);
    }

    public function testSyncRolesWithActorEnforcesAssignableRoleFreeze(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listActiveRoleIds')->willReturn([1, 2, 3, 4]);

        $userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepo->expects($this->once())->method('listRoleIdsByUserId')->with(10)->willReturn([2, 4]);
        $userRoleRepo->expects($this->once())
            ->method('replaceForUser')
            ->with(10, [4, 1]) // frozen(4) + touched(1)
            ->willReturn(true);

        $assignableRoleService = $this->createMock(AssignableRoleService::class);
        $assignableRoleService->expects($this->once())
            ->method('computeEffectiveRoleIds')
            ->with(99, [1, 3], [2, 4])
            ->willReturn([4, 1]); // role 4 frozen, role 1 submitted & assignable

        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())->method('clearUserCache')->with(10);

        $service = $this->newService(
            userRoleRepository: $userRoleRepo,
            directoryGateway: $directoryGateway,
            permissionService: $permissionService,
            assignableRoleService: $assignableRoleService
        );

        $result = $service->syncRoles(10, [1, 3], true, 99);
        $this->assertTrue($result);
    }

    public function testSyncRolesClearsPermissionCacheEvenOnFailure(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listActiveRoleIds')->willReturn([1]);

        $userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepo->expects($this->once())->method('replaceForUser')->willReturn(false);

        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->expects($this->once())->method('clearUserCache')->with(10);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->never())->method('bumpAuthzVersion');

        $service = $this->newService(
            userWriteRepository: $userWriteRepo,
            userRoleRepository: $userRoleRepo,
            directoryGateway: $directoryGateway,
            permissionService: $permissionService
        );

        $result = $service->syncRoles(10, [1]);
        $this->assertFalse($result);
    }

    // ── syncDepartments ─────────────────────────────────────────────────

    public function testSyncDepartmentsValidatesDepartmentsBelongToUserTenants(): void
    {
        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->expects($this->once())->method('listTenantIdsByUserId')->with(10)->willReturn([1]);

        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->once())
            ->method('listActiveDepartmentIdsByTenantIds')
            ->with([1])
            ->willReturn([100, 101]);

        $userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
        $userDeptRepo->expects($this->once())
            ->method('replaceForUser')
            ->with(10, [100]) // 200 filtered out because not in allowed list
            ->willReturn(true);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);

        $service = $this->newService(
            userWriteRepository: $userWriteRepo,
            userTenantRepository: $userTenantRepo,
            userDepartmentRepository: $userDeptRepo,
            directoryGateway: $directoryGateway
        );

        $result = $service->syncDepartments(10, [100, 200]);
        $this->assertTrue($result);
    }

    public function testSyncDepartmentsRejectsAllWhenUserHasNoTenants(): void
    {
        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->expects($this->once())->method('listTenantIdsByUserId')->with(10)->willReturn([]);

        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->never())->method('listActiveDepartmentIdsByTenantIds');

        $userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
        $userDeptRepo->expects($this->once())
            ->method('replaceForUser')
            ->with(10, [])
            ->willReturn(true);

        $service = $this->newService(
            userTenantRepository: $userTenantRepo,
            userDepartmentRepository: $userDeptRepo,
            directoryGateway: $directoryGateway
        );

        $result = $service->syncDepartments(10, [100]);
        $this->assertTrue($result);
    }

    public function testSyncDepartmentsRejectsAllWhenNoActiveDepartmentsInTenants(): void
    {
        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);

        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listActiveDepartmentIdsByTenantIds')->willReturn([]);

        $userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
        $userDeptRepo->expects($this->once())
            ->method('replaceForUser')
            ->with(10, [])
            ->willReturn(true);

        $service = $this->newService(
            userTenantRepository: $userTenantRepo,
            userDepartmentRepository: $userDeptRepo,
            directoryGateway: $directoryGateway
        );

        $result = $service->syncDepartments(10, [100]);
        $this->assertTrue($result);
    }

    // ── syncAllAssignments ──────────────────────────────────────────────

    public function testSyncAllAssignmentsReturnsFalseWhenUserIdIsZero(): void
    {
        $service = $this->newService();
        $this->assertFalse($service->syncAllAssignments(0, [], [], []));
    }

    public function testSyncAllAssignmentsCommitsTransactionOnSuccess(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listTenantIds')->willReturn([1]);
        $directoryGateway->method('listActiveRoleIds')->willReturn([2]);
        $directoryGateway->method('listActiveDepartmentIdsByTenantIds')->willReturn([3]);

        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->method('replaceForUser')->willReturn(true);
        $userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);

        $userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepo->method('replaceForUser')->willReturn(true);

        $userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
        $userDeptRepo->method('replaceForUser')->willReturn(true);

        $permissionService = $this->createMock(PermissionService::class);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);

        $dbSession = $this->createMock(DatabaseSessionRepository::class);
        $dbSession->expects($this->once())->method('beginTransaction');
        $dbSession->expects($this->once())->method('commitTransaction');
        $dbSession->expects($this->never())->method('rollbackTransaction');

        $service = $this->newService(
            userWriteRepository: $userWriteRepo,
            userTenantRepository: $userTenantRepo,
            userRoleRepository: $userRoleRepo,
            userDepartmentRepository: $userDeptRepo,
            directoryGateway: $directoryGateway,
            permissionService: $permissionService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->syncAllAssignments(10, [1], [2], [3]);
        $this->assertTrue($result);
    }

    public function testSyncAllAssignmentsRollsBackOnSyncFailure(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listTenantIds')->willReturn([1]);

        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->method('replaceForUser')->willReturn(false); // tenant sync fails

        $dbSession = $this->createMock(DatabaseSessionRepository::class);
        $dbSession->expects($this->once())->method('beginTransaction');
        $dbSession->expects($this->never())->method('commitTransaction');
        $dbSession->expects($this->once())->method('rollbackTransaction');

        $service = $this->newService(
            userTenantRepository: $userTenantRepo,
            directoryGateway: $directoryGateway,
            databaseSessionRepository: $dbSession
        );

        $result = $service->syncAllAssignments(10, [1], [2], [3]);
        $this->assertFalse($result);
    }

    public function testSyncAllAssignmentsRollsBackOnException(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listTenantIds')
            ->willThrowException(new \RuntimeException('db error'));

        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);

        $dbSession = $this->createMock(DatabaseSessionRepository::class);
        $dbSession->expects($this->once())->method('beginTransaction');
        $dbSession->expects($this->never())->method('commitTransaction');
        $dbSession->expects($this->once())->method('rollbackTransaction');

        $service = $this->newService(
            userTenantRepository: $userTenantRepo,
            directoryGateway: $directoryGateway,
            databaseSessionRepository: $dbSession
        );

        $result = $service->syncAllAssignments(10, [1], [], []);
        $this->assertFalse($result);
    }

    public function testSyncAllAssignmentsBumpsAuthzOnlyOnce(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listTenantIds')->willReturn([1]);
        $directoryGateway->method('listActiveRoleIds')->willReturn([2]);
        $directoryGateway->method('listActiveDepartmentIdsByTenantIds')->willReturn([3]);

        $userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenantRepo->method('replaceForUser')->willReturn(true);
        $userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);

        $userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
        $userRoleRepo->method('replaceForUser')->willReturn(true);

        $userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
        $userDeptRepo->method('replaceForUser')->willReturn(true);

        $permissionService = $this->createMock(PermissionService::class);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        // bumpAuthzVersion should be called exactly once (not three times)
        $userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);

        $dbSession = $this->createMock(DatabaseSessionRepository::class);

        $service = $this->newService(
            userWriteRepository: $userWriteRepo,
            userTenantRepository: $userTenantRepo,
            userRoleRepository: $userRoleRepo,
            userDepartmentRepository: $userDeptRepo,
            directoryGateway: $directoryGateway,
            permissionService: $permissionService,
            databaseSessionRepository: $dbSession
        );

        $service->syncAllAssignments(10, [1], [2], [3]);
    }

    // ── bumpAuthzVersion ────────────────────────────────────────────────

    public function testBumpAuthzVersionReturnsEarlyWhenUserIdIsZero(): void
    {
        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->never())->method('bumpAuthzVersion');

        $service = $this->newService(userWriteRepository: $userWriteRepo);
        $service->bumpAuthzVersion(0);
    }

    public function testBumpAuthzVersionReturnsEarlyWhenUserIdIsNegative(): void
    {
        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->never())->method('bumpAuthzVersion');

        $service = $this->newService(userWriteRepository: $userWriteRepo);
        $service->bumpAuthzVersion(-1);
    }

    public function testBumpAuthzVersionCallsRepository(): void
    {
        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(42);

        $service = $this->newService(userWriteRepository: $userWriteRepo);
        $service->bumpAuthzVersion(42);
    }

    // ── normalizeIdInput ────────────────────────────────────────────────

    public function testNormalizeIdInputWithScalarInt(): void
    {
        $service = $this->newService();
        $this->assertSame([5], $service->normalizeIdInput(5));
    }

    public function testNormalizeIdInputWithArrayOfInts(): void
    {
        $service = $this->newService();
        $this->assertSame([1, 2, 3], $service->normalizeIdInput([1, 2, 3]));
    }

    public function testNormalizeIdInputWithCommaSeparatedString(): void
    {
        $service = $this->newService();
        $this->assertSame([1, 2, 3], $service->normalizeIdInput('1,2,3'));
    }

    public function testNormalizeIdInputWithNestedArrays(): void
    {
        $service = $this->newService();
        $this->assertSame([1, 2, 3], $service->normalizeIdInput([[1, [2]], 3]));
    }

    public function testNormalizeIdInputFiltersNegativeValues(): void
    {
        $service = $this->newService();
        $this->assertSame([1, 3], $service->normalizeIdInput([1, -2, 3, 0]));
    }

    public function testNormalizeIdInputRemovesDuplicates(): void
    {
        $service = $this->newService();
        $this->assertSame([1, 2], $service->normalizeIdInput([1, 2, 1, 2]));
    }

    public function testNormalizeIdInputHandlesEmptyString(): void
    {
        $service = $this->newService();
        $this->assertSame([], $service->normalizeIdInput(''));
    }

    public function testNormalizeIdInputHandlesEmptyArray(): void
    {
        $service = $this->newService();
        $this->assertSame([], $service->normalizeIdInput([]));
    }

    // ── normalizeTenantIds ──────────────────────────────────────────────

    public function testNormalizeTenantIdsFiltersAgainstValidTenantIds(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->once())->method('listTenantIds')->willReturn([1, 2, 3]);

        $service = $this->newService(directoryGateway: $directoryGateway);
        $result = $service->normalizeTenantIds([1, 99, 2]);

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

    public function testNormalizeTenantIdsFiltersNegativeAndZeroIds(): void
    {
        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->method('listTenantIds')->willReturn([1, 2]);

        $service = $this->newService(directoryGateway: $directoryGateway);
        $result = $service->normalizeTenantIds([0, -1, 1]);

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

    // ── Helper ──────────────────────────────────────────────────────────

    private function newService(
        ?UserWriteRepositoryInterface $userWriteRepository = null,
        ?UserTenantRepositoryInterface $userTenantRepository = null,
        ?UserRoleRepositoryInterface $userRoleRepository = null,
        ?UserDepartmentRepositoryInterface $userDepartmentRepository = null,
        ?UserDirectoryGateway $directoryGateway = null,
        ?PermissionService $permissionService = null,
        ?DatabaseSessionRepository $databaseSessionRepository = null,
        ?AssignableRoleService $assignableRoleService = null,
    ): UserAssignmentService {
        return new UserAssignmentService(
            $userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class),
            $userTenantRepository ?? $this->createMock(UserTenantRepositoryInterface::class),
            $userRoleRepository ?? $this->createMock(UserRoleRepositoryInterface::class),
            $userDepartmentRepository ?? $this->createMock(UserDepartmentRepositoryInterface::class),
            $directoryGateway ?? $this->createMock(UserDirectoryGateway::class),
            $permissionService ?? $this->createMock(PermissionService::class),
            $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class),
            $assignableRoleService ?? $this->createMock(AssignableRoleService::class),
        );
    }
}
