<?php

namespace MintyPHP\Tests\Service\User;

use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
use MintyPHP\Service\User\UserLifecycleService;
use MintyPHP\Service\User\UserSettingsGateway;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class UserLifecycleServiceTest extends TestCase
{
    private UserReadRepositoryInterface&MockObject $userReadRepository;
    private UserWriteRepositoryInterface&MockObject $userWriteRepository;
    private UserSettingsGateway&MockObject $settingsGateway;
    private UserLifecycleAuditInterface&MockObject $auditService;
    private DatabaseSessionRepository&MockObject $databaseSessionRepository;

    protected function setUp(): void
    {
        $this->userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $this->userWriteRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $this->settingsGateway = $this->createMock(UserSettingsGateway::class);
        $this->auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);

        // Default: lock succeeds, release is a no-op
        $this->databaseSessionRepository->method('acquireAdvisoryLock')->willReturn(true);
        $this->databaseSessionRepository->method('releaseAdvisoryLock');

        // Default: no privileged users
        $this->userReadRepository->method('listPrivilegedUserIdsByPermissionKeys')->willReturn([]);
    }

    private function createService(): UserLifecycleService
    {
        return new UserLifecycleService(
            $this->userReadRepository,
            $this->userWriteRepository,
            $this->settingsGateway,
            $this->auditService,
            $this->databaseSessionRepository
        );
    }

    public function testRunReturnsLockedWhenLockNotAcquired(): void
    {
        $this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
        $this->databaseSessionRepository->method('acquireAdvisoryLock')->willReturn(false);

        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);

        $this->userWriteRepository->expects($this->never())->method('setInactiveByIds');
        $this->userWriteRepository->expects($this->never())->method('deleteByIds');

        $result = $this->createService()->run();

        $this->assertFalse($result['ok']);
        $this->assertTrue($result['locked']);
        $this->assertSame('lock_not_acquired', $result['error']);
    }

    public function testRunSkipsWhenDeactivateDaysIsZero(): void
    {
        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(0);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);

        $this->userReadRepository->expects($this->never())->method('listIdsForAutoDeactivate');
        $this->userReadRepository->expects($this->never())->method('listIdsForAutoDelete');
        $this->userWriteRepository->expects($this->never())->method('setInactiveByIds');
        $this->userWriteRepository->expects($this->never())->method('deleteByIds');

        $result = $this->createService()->run();

        $this->assertTrue($result['ok']);
        $this->assertSame(0, $result['deactivated_count']);
        $this->assertSame(0, $result['deleted_count']);
        $this->assertSame(0, $result['policy']['deactivate_days']);
        $this->assertSame(0, $result['policy']['delete_days']);
    }

    public function testRunDeactivatesUsers(): void
    {
        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(0);

        $this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([1, 2]);

        $user1 = ['id' => 1, 'email' => 'a@example.com'];
        $user2 = ['id' => 2, 'email' => 'b@example.com'];
        $this->userReadRepository->method('find')->willReturnCallback(
            fn (int $id) => match ($id) {
                1 => $user1,
                2 => $user2,
                default => null,
            }
        );

        $this->userWriteRepository->expects($this->once())
            ->method('setInactiveByIds')
            ->with([1, 2], null)
            ->willReturn(2);

        $this->userWriteRepository->expects($this->once())
            ->method('bumpAuthzVersionByUserIds')
            ->with([1, 2]);

        $this->auditService->expects($this->exactly(2))->method('logDeactivate');

        $result = $this->createService()->run();

        $this->assertTrue($result['ok']);
        $this->assertSame(2, $result['deactivated_count']);
    }

    public function testRunDeletesUsers(): void
    {
        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);

        $this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([]);
        $this->userReadRepository->method('listIdsForAutoDelete')->willReturn([3]);

        $user3 = ['id' => 3, 'email' => 'c@example.com'];
        $this->userReadRepository->method('find')->willReturn($user3);

        $this->auditService->expects($this->once())
            ->method('logDeleteWithSnapshot')
            ->willReturn(42);

        $this->userWriteRepository->expects($this->once())
            ->method('deleteByIds')
            ->with([3])
            ->willReturn(1);

        $result = $this->createService()->run();

        $this->assertTrue($result['ok']);
        $this->assertSame(1, $result['deleted_count']);
        $this->assertSame(0, $result['skipped_count']);
    }

    public function testRunSkipsDeleteWhenSnapshotFails(): void
    {
        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);

        $this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([]);
        $this->userReadRepository->method('listIdsForAutoDelete')->willReturn([5]);

        $user5 = ['id' => 5, 'email' => 'e@example.com'];
        $this->userReadRepository->method('find')->willReturn($user5);

        $this->auditService->expects($this->once())
            ->method('logDeleteWithSnapshot')
            ->willReturn(0);

        $this->auditService->expects($this->once())
            ->method('logDeleteFailure');

        $this->userWriteRepository->expects($this->never())->method('deleteByIds');

        $result = $this->createService()->run();

        $this->assertSame(1, $result['skipped_count']);
        $this->assertSame(0, $result['deleted_count']);
    }

    public function testRunReleasesLockEvenOnException(): void
    {
        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(0);

        $this->userReadRepository->method('listIdsForAutoDeactivate')
            ->willThrowException(new \RuntimeException('DB error'));

        $this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
        $this->databaseSessionRepository->method('acquireAdvisoryLock')->willReturn(true);
        $this->databaseSessionRepository->expects($this->once())->method('releaseAdvisoryLock');

        $result = $this->createService()->run();

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

    public function testRunReportsPrivilegedUserCount(): void
    {
        $this->userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $this->userReadRepository->method('listPrivilegedUserIdsByPermissionKeys')
            ->willReturn([10, 20, 30]);
        $this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([]);
        $this->userReadRepository->method('listIdsForAutoDelete')->willReturn([]);

        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);

        $result = $this->createService()->run();

        $this->assertSame(3, $result['skipped_privileged_count']);
    }

    public function testRunTriggerTypeIsManualWhenActorProvided(): void
    {
        $this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
        $this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(0);

        $this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([7]);

        $user7 = ['id' => 7, 'email' => 'g@example.com'];
        $this->userReadRepository->method('find')->willReturn($user7);

        $this->userWriteRepository->method('setInactiveByIds')->willReturn(1);

        $this->auditService->expects($this->once())
            ->method('logDeactivate')
            ->with(
                $this->isString(),
                'manual',
                $this->isArray(),
                99,
                $user7,
                'success',
                null
            );

        $result = $this->createService()->run(99);

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