<?php

namespace MintyPHP\Tests\Service\User;

use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\User\UserListQueryRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Tenant\TenantScopeService;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserAssignmentService;
use MintyPHP\Service\User\UserDirectoryGateway;
use MintyPHP\Service\User\UserPasswordService;
use MintyPHP\Service\User\UserSettingsGateway;
use PHPUnit\Framework\TestCase;

class UserAccountServiceTest extends TestCase
{
    public function testListPagedDropsTenantUserIdForGlobalAccess(): void
    {
        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->once())
            ->method('hasGlobalAccess')
            ->with(99)
            ->willReturn(true);

        $listQueryRepository = $this->createMock(UserListQueryRepositoryInterface::class);
        $listQueryRepository->expects($this->once())
            ->method('listPaged')
            ->with($this->callback(static function (array $options): bool {
                return !isset($options['tenantUserId'])
                    && (int) ($options['page'] ?? 0) === 2;
            }))
            ->willReturn(['rows' => [], 'total' => 0]);

        $service = $this->newService(null, null, $listQueryRepository, null, null, null, $scopeGateway);
        $result = $service->listPaged(['page' => 2, 'tenantUserId' => 99]);

        $this->assertSame(0, (int) ($result['total'] ?? -1));
    }

    public function testDeleteByUuidReturnsNotFoundForEmptyUuid(): void
    {
        $service = $this->newService();

        $result = $service->deleteByUuid('');

        $this->assertFalse($result['ok']);
        $this->assertSame(404, (int) ($result['status'] ?? 0));
        $this->assertSame('not_found', (string) ($result['error'] ?? ''));
    }

    public function testDeleteByUuidPreventsSelfDelete(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->once())
            ->method('findByUuid')
            ->with('user-uuid')
            ->willReturn(['id' => 10, 'uuid' => 'user-uuid']);

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

        $service = $this->newService($readRepository, $writeRepository);
        $result = $service->deleteByUuid('user-uuid', 10);

        $this->assertFalse($result['ok']);
        $this->assertSame(400, (int) ($result['status'] ?? 0));
        $this->assertSame('self_delete', (string) ($result['error'] ?? ''));
    }

    public function testDeleteByUuidDeletesAndRecordsAuditEvent(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->once())
            ->method('findByUuid')
            ->with('user-uuid')
            ->willReturn(['id' => 11, 'uuid' => 'user-uuid']);

        $writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $writeRepository->expects($this->once())
            ->method('delete')
            ->with(11)
            ->willReturn(true);

        $auditService = $this->createMock(SystemAuditService::class);
        $auditService->expects($this->once())
            ->method('record')
            ->with(
                'admin.users.delete',
                'success',
                $this->callback(static function (array $context): bool {
                    return (int) ($context['actor_user_id'] ?? 0) === 44
                        && (int) ($context['target_id'] ?? 0) === 11
                        && (string) ($context['target_uuid'] ?? '') === 'user-uuid';
                })
            );

        $service = $this->newService($readRepository, $writeRepository, null, null, null, null, null, null, $auditService);
        $result = $service->deleteByUuid('user-uuid', 44);

        $this->assertTrue($result['ok']);
        $this->assertSame(11, (int) (($result['user'] ?? [])['id'] ?? 0));
    }

    public function testDeleteByUuidsReturnsPermissionDeniedWhenNothingIsInScope(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->method('findByUuid')->willReturn(['id' => 51, 'uuid' => 'other-uuid']);

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

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

        $service = $this->newService($readRepository, $writeRepository, null, null, null, null, $scopeGateway);
        $result = $service->deleteByUuids(['other-uuid'], 99);

        $this->assertFalse($result['ok']);
        $this->assertSame('permission_denied', (string) ($result['error'] ?? ''));
    }

    public function testDeleteByUuidsReturnsSelfDeleteWhenOnlyOwnUuidRemains(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->once())
            ->method('findByUuid')
            ->with('self-uuid')
            ->willReturn(['id' => 99, 'uuid' => 'self-uuid']);
        $readRepository->expects($this->once())
            ->method('find')
            ->with(99)
            ->willReturn(['id' => 99, 'uuid' => 'self-uuid']);

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

        $service = $this->newService($readRepository, $writeRepository);
        $result = $service->deleteByUuids(['self-uuid'], 99);

        $this->assertFalse($result['ok']);
        $this->assertSame('self_delete', (string) ($result['error'] ?? ''));
    }

    public function testSetActiveByUuidPreventsSelfDeactivate(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->method('findByUuid')->willReturn(['id' => 42, 'uuid' => 'u-42']);

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

        $service = $this->newService($readRepository, $writeRepository);
        $result = $service->setActiveByUuid('u-42', false, 42);

        $this->assertFalse($result['ok']);
        $this->assertSame(400, (int) ($result['status'] ?? 0));
        $this->assertSame('self_deactivate', (string) ($result['error'] ?? ''));
    }

    public function testSetActiveByUuidUpdatesAssignmentsAndAudit(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->method('findByUuid')->willReturn(['id' => 33, 'uuid' => 'u-33', 'active' => 0]);

        $writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $writeRepository->expects($this->once())
            ->method('setActive')
            ->with(33, true, 90)
            ->willReturn(true);

        $assignmentService = $this->createMock(UserAssignmentService::class);
        $assignmentService->expects($this->once())
            ->method('bumpAuthzVersion')
            ->with(33);

        $auditService = $this->createMock(SystemAuditService::class);
        $auditService->expects($this->once())
            ->method('record')
            ->with(
                'admin.users.activate',
                'success',
                $this->callback(static fn ($context): bool => is_array($context))
            );

        $service = $this->newService($readRepository, $writeRepository, null, $assignmentService, null, null, null, null, $auditService);
        $result = $service->setActiveByUuid('u-33', true, 90);

        $this->assertTrue($result['ok']);
    }

    public function testSetActiveByUuidsBumpsAuthzVersionForResolvedUserIds(): void
    {
        $writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $writeRepository->expects($this->once())
            ->method('setActiveByUuids')
            ->with(['u-1', 'u-2'], false, 7)
            ->willReturn(true);
        $writeRepository->expects($this->once())
            ->method('bumpAuthzVersionByUserIds')
            ->with([11, 12])
            ->willReturn(2);

        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->method('findByUuid')->willReturnMap([
            ['u-1', ['id' => 11, 'uuid' => 'u-1']],
            ['u-2', ['id' => 12, 'uuid' => 'u-2']],
        ]);

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

        $auditService = $this->createMock(SystemAuditService::class);
        $auditService->expects($this->once())
            ->method('record')
            ->with(
                'admin.users.bulk_update',
                'success',
                $this->callback(static fn ($context): bool => is_array($context))
            );

        $service = $this->newService($readRepository, $writeRepository, null, null, null, null, $scopeGateway, null, $auditService);
        $result = $service->setActiveByUuids(['u-1', 'u-2'], false, 7);

        $this->assertTrue($result['ok']);
        $this->assertSame(2, (int) ($result['count'] ?? 0));
    }

    public function testRegisterCreatesUserAndAssignsDefaultEntities(): void
    {
        $passwordService = $this->createMock(UserPasswordService::class);
        $passwordService->method('validatePassword')->willReturn([]);

        $settingsGateway = $this->createMock(UserSettingsGateway::class);
        $settingsGateway->method('getAppTheme')->willReturn('dark');
        $settingsGateway->method('getDefaultTenantId')->willReturn(3);
        $settingsGateway->method('getDefaultRoleId')->willReturn(4);
        $settingsGateway->method('getDefaultDepartmentId')->willReturn(5);

        $writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $writeRepository->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $data): bool {
                return (string) ($data['email'] ?? '') === 'user@example.com'
                    && (string) ($data['theme'] ?? '') === 'dark'
                    && (int) ($data['primary_tenant_id'] ?? 0) === 3
                    && (int) ($data['active'] ?? 0) === 1;
            }))
            ->willReturn(77);

        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->exactly(2))
            ->method('findByEmail')
            ->with('user@example.com')
            ->willReturnOnConsecutiveCalls(
                null,
                ['id' => 77, 'uuid' => 'user-uuid']
            );

        $assignmentService = $this->createMock(UserAssignmentService::class);
        $assignmentService->expects($this->once())->method('syncTenants')->with(77, [3])->willReturn(true);
        $assignmentService->expects($this->once())->method('syncRoles')->with(77, [4])->willReturn(true);
        $assignmentService->expects($this->once())->method('syncDepartments')->with(77, [5])->willReturn(true);

        $service = $this->newService(
            $readRepository,
            $writeRepository,
            null,
            $assignmentService,
            $passwordService,
            $settingsGateway
        );

        $result = $service->register([
            'first_name' => 'Max',
            'last_name' => 'Mustermann',
            'email' => 'user@example.com',
            'password' => 'StrongPass1!',
            'password2' => 'StrongPass1!',
        ]);

        $this->assertTrue($result['ok']);
    }

    public function testCreateFromAdminRollsBackTransactionWhenTenantSyncFails(): void
    {
        $passwordService = $this->createMock(UserPasswordService::class);
        $passwordService->method('validatePassword')->willReturn([]);

        $settingsGateway = $this->createMock(UserSettingsGateway::class);
        $settingsGateway->method('getDefaultTenantId')->willReturn(null);
        $settingsGateway->method('getDefaultRoleId')->willReturn(null);
        $settingsGateway->method('getDefaultDepartmentId')->willReturn(null);
        $settingsGateway->method('getAppTheme')->willReturn('light');

        $writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $writeRepository->expects($this->once())->method('create')->willReturn(88);

        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->exactly(2))
            ->method('findByEmail')
            ->with('create@example.com')
            ->willReturnOnConsecutiveCalls(
                null,
                ['id' => 88, 'uuid' => 'user-uuid-88']
            );

        $assignmentService = $this->createMock(UserAssignmentService::class);
        $assignmentService->expects($this->once())->method('normalizeTenantIds')->with([3])->willReturn([3]);
        $assignmentService->expects($this->once())->method('syncTenants')->with(88, [3])->willReturn(false);
        $assignmentService->expects($this->never())->method('syncRoles');
        $assignmentService->expects($this->never())->method('syncDepartments');

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

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

        $service = $this->newService(
            $readRepository,
            $writeRepository,
            null,
            $assignmentService,
            $passwordService,
            $settingsGateway,
            null,
            null,
            $auditService,
            $databaseSessionRepository
        );

        $result = $service->createFromAdmin([
            'first_name' => 'Max',
            'last_name' => 'Mustermann',
            'email' => 'create@example.com',
            'password' => 'StrongPass1!',
            'password2' => 'StrongPass1!',
            'tenant_ids' => [3],
            'active' => 1,
        ], 42);

        $this->assertFalse($result['ok']);
        $this->assertNotEmpty($result['errors'] ?? []);
    }

    public function testRegisterRollsBackTransactionWhenRoleSyncFails(): void
    {
        $passwordService = $this->createMock(UserPasswordService::class);
        $passwordService->method('validatePassword')->willReturn([]);

        $settingsGateway = $this->createMock(UserSettingsGateway::class);
        $settingsGateway->method('getAppTheme')->willReturn('dark');
        $settingsGateway->method('getDefaultTenantId')->willReturn(3);
        $settingsGateway->method('getDefaultRoleId')->willReturn(4);
        $settingsGateway->method('getDefaultDepartmentId')->willReturn(5);

        $writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $writeRepository->expects($this->once())->method('create')->willReturn(77);

        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->exactly(2))
            ->method('findByEmail')
            ->with('user@example.com')
            ->willReturnOnConsecutiveCalls(
                null,
                ['id' => 77, 'uuid' => 'user-uuid']
            );

        $assignmentService = $this->createMock(UserAssignmentService::class);
        $assignmentService->expects($this->once())->method('syncTenants')->with(77, [3])->willReturn(true);
        $assignmentService->expects($this->once())->method('syncRoles')->with(77, [4])->willReturn(false);
        $assignmentService->expects($this->never())->method('syncDepartments');

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

        $service = $this->newService(
            $readRepository,
            $writeRepository,
            null,
            $assignmentService,
            $passwordService,
            $settingsGateway,
            null,
            null,
            null,
            $databaseSessionRepository
        );

        $result = $service->register([
            'first_name' => 'Max',
            'last_name' => 'Mustermann',
            'email' => 'user@example.com',
            'password' => 'StrongPass1!',
            'password2' => 'StrongPass1!',
        ]);

        $this->assertFalse($result['ok']);
        $this->assertNotSame('', trim((string) ($result['error'] ?? '')));
    }

    public function testUpdateSelfProfileReturnsErrorWhenRequestedTenantIsNotAssigned(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->once())
            ->method('find')
            ->with(15)
            ->willReturn([
                'id' => 15,
                'first_name' => 'Max',
                'last_name' => 'Mustermann',
                'locale' => 'de',
                'theme' => 'light',
            ]);

        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->once())
            ->method('findTenantByUuid')
            ->with('tenant-uuid-2')
            ->willReturn(['id' => 2, 'uuid' => 'tenant-uuid-2']);

        $assignmentService = $this->createMock(UserAssignmentService::class);
        $assignmentService->expects($this->once())
            ->method('buildAssignmentsForUser')
            ->with(15)
            ->willReturn([
                'tenants' => [['id' => 1, 'uuid' => 'tenant-uuid-1']],
                'roles' => [],
                'departments' => [],
            ]);

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

        $service = $this->newService(
            $readRepository,
            $writeRepository,
            null,
            $assignmentService,
            null,
            null,
            null,
            $directoryGateway
        );

        $result = $service->updateSelfProfile(15, [
            'first_name' => 'Max',
            'last_name' => 'Mustermann',
            'current_tenant_uuid' => 'tenant-uuid-2',
        ]);

        $this->assertFalse($result['ok']);
        $this->assertNotEmpty($result['errors']);
    }

    public function testUpdateSelfProfilePersistsDataAndTenantSwitchWhenAllowed(): void
    {
        $readRepository = $this->createMock(UserReadRepositoryInterface::class);
        $readRepository->expects($this->once())
            ->method('find')
            ->with(21)
            ->willReturn([
                'id' => 21,
                'first_name' => 'Max',
                'last_name' => 'Mustermann',
                'profile_description' => '',
                'job_title' => '',
                'phone' => '',
                'mobile' => '',
                'short_dial' => '',
                'address' => '',
                'postal_code' => '',
                'city' => '',
                'country' => '',
                'region' => '',
                'locale' => 'de',
                'theme' => 'light',
            ]);

        $directoryGateway = $this->createMock(UserDirectoryGateway::class);
        $directoryGateway->expects($this->once())
            ->method('findTenantByUuid')
            ->with('tenant-uuid-9')
            ->willReturn(['id' => 9, 'uuid' => 'tenant-uuid-9']);

        $assignmentService = $this->createMock(UserAssignmentService::class);
        $assignmentService->expects($this->once())
            ->method('buildAssignmentsForUser')
            ->with(21)
            ->willReturn([
                'tenants' => [['id' => 9, 'uuid' => 'tenant-uuid-9']],
                'roles' => [],
                'departments' => [],
            ]);

        $writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $writeRepository->expects($this->once())
            ->method('update')
            ->with(21, $this->callback(static fn ($payload): bool => is_array($payload)))
            ->willReturn(true);
        $writeRepository->expects($this->once())
            ->method('setCurrentTenant')
            ->with(21, 9)
            ->willReturn(true);

        $service = $this->newService(
            $readRepository,
            $writeRepository,
            null,
            $assignmentService,
            null,
            null,
            null,
            $directoryGateway
        );

        $result = $service->updateSelfProfile(21, [
            'first_name' => 'Max',
            'last_name' => 'Mustermann',
            'current_tenant_uuid' => 'tenant-uuid-9',
            'locale' => 'de',
            'theme' => 'light',
        ]);

        $this->assertTrue($result['ok']);
    }

    private function newService(
        ?UserReadRepositoryInterface $readRepository = null,
        ?UserWriteRepositoryInterface $writeRepository = null,
        ?UserListQueryRepositoryInterface $listQueryRepository = null,
        ?UserAssignmentService $assignmentService = null,
        ?UserPasswordService $passwordService = null,
        ?UserSettingsGateway $settingsGateway = null,
        ?TenantScopeService $scopeGateway = null,
        ?UserDirectoryGateway $directoryGateway = null,
        ?SystemAuditService $auditService = null,
        ?DatabaseSessionRepository $databaseSessionRepository = null
    ): UserAccountService {
        $readRepository = $readRepository ?? $this->createMock(UserReadRepositoryInterface::class);
        $writeRepository = $writeRepository ?? $this->createMock(UserWriteRepositoryInterface::class);
        $listQueryRepository = $listQueryRepository ?? $this->createMock(UserListQueryRepositoryInterface::class);

        if ($assignmentService === null) {
            $assignmentService = $this->createMock(UserAssignmentService::class);
            $assignmentService->method('normalizeTenantIds')->willReturn([]);
            $assignmentService->method('normalizeIdInput')->willReturn([]);
            $assignmentService->method('buildAssignmentsForUser')->willReturn([
                'tenants' => [],
                'roles' => [],
                'departments' => [],
            ]);
            $assignmentService->method('syncTenants')->willReturn(true);
            $assignmentService->method('syncRoles')->willReturn(true);
            $assignmentService->method('syncDepartments')->willReturn(true);
        }

        if ($passwordService === null) {
            $passwordService = $this->createMock(UserPasswordService::class);
            $passwordService->method('validatePassword')->willReturn([]);
        }

        if ($settingsGateway === null) {
            $settingsGateway = $this->createMock(UserSettingsGateway::class);
            $settingsGateway->method('getDefaultTenantId')->willReturn(null);
            $settingsGateway->method('getDefaultRoleId')->willReturn(null);
            $settingsGateway->method('getDefaultDepartmentId')->willReturn(null);
            $settingsGateway->method('getAppTheme')->willReturn('light');
        }

        if ($scopeGateway === null) {
            $scopeGateway = $this->createMock(TenantScopeService::class);
            $scopeGateway->method('hasGlobalAccess')->willReturn(false);
            $scopeGateway->method('canAccess')->willReturn(false);
        }

        if ($directoryGateway === null) {
            $directoryGateway = $this->createMock(UserDirectoryGateway::class);
            $directoryGateway->method('findTenantByUuid')->willReturn(null);
        }

        $auditService = $auditService ?? $this->createMock(SystemAuditService::class);
        $databaseSessionRepository = $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class);

        return new UserAccountService(
            $readRepository,
            $writeRepository,
            $listQueryRepository,
            $assignmentService,
            $passwordService,
            $settingsGateway,
            $scopeGateway,
            $directoryGateway,
            $auditService,
            $databaseSessionRepository
        );
    }
}
