<?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\UserLifecycleRestoreService;
use MintyPHP\Service\User\UserSettingsGateway;
use PHPUnit\Framework\TestCase;

class UserLifecycleRestoreServiceTest extends TestCase
{
    // ── Full success path ───────────────────────────────────────────────

    public function testRestoreFullSuccessPath(): void
    {
        $event = [
            'id' => 1,
            'restored_at' => null,
            'snapshot_enc' => 'enc-data',
            'policy_deactivate_days' => 30,
            'policy_delete_days' => 90,
        ];

        $snapshot = [
            'uuid' => 'restored-uuid',
            'email' => 'restored@example.com',
            'first_name' => 'Max',
            'last_name' => 'Mustermann',
            'theme' => 'dark',
        ];

        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->expects($this->once())
            ->method('findDeleteEventForRestore')
            ->with(1, true)
            ->willReturn($event);
        $auditService->expects($this->once())
            ->method('decryptSnapshot')
            ->with($event)
            ->willReturn($snapshot);
        $auditService->expects($this->once())
            ->method('markDeleteEventRestored')
            ->with(1, 99, 77)
            ->willReturn(true);
        $auditService->expects($this->once())
            ->method('logRestore')
            ->with(
                $this->isString(),
                'manual',
                $this->callback(static fn (array $policy): bool => $policy['deactivate_days'] === 30 && $policy['delete_days'] === 90),
                99,
                $this->callback(static fn (array $user): bool => $user['id'] === 77 && $user['uuid'] === 'restored-uuid' && $user['email'] === 'restored@example.com'),
                'success',
                null
            );

        $userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepo->expects($this->once())->method('findByUuid')->with('restored-uuid')->willReturn(null);
        $userReadRepo->expects($this->once())->method('findByEmail')->with('restored@example.com')->willReturn(null);

        $userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $data): bool {
                return $data['uuid'] === 'restored-uuid'
                    && $data['email'] === 'restored@example.com'
                    && $data['first_name'] === 'Max'
                    && $data['last_name'] === 'Mustermann'
                    && $data['active'] === 0
                    && $data['created_by'] === 99
                    && $data['primary_tenant_id'] === null
                    && $data['current_tenant_id'] === null
                    && $data['totp_secret'] === '';
            }))
            ->willReturn(77);

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

        $settingsGateway = $this->createMock(UserSettingsGateway::class);
        $settingsGateway->method('normalizeTheme')->willReturn('dark');

        $service = $this->newService(
            userReadRepository: $userReadRepo,
            userWriteRepository: $userWriteRepo,
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession,
            userSettingsGateway: $settingsGateway
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

        $this->assertTrue($result['ok']);
        $this->assertSame(77, $result['restored_user_id']);
        $this->assertSame('restored-uuid', $result['restored_user_uuid']);
        $this->assertSame(1, $result['audit_id']);
    }

    // ── invalid_request ─────────────────────────────────────────────────

    public function testRestoreReturnsInvalidRequestWhenAuditIdIsZero(): void
    {
        $service = $this->newService();
        $result = $service->restoreFromLifecycleAudit(0, 99);

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

    public function testRestoreReturnsInvalidRequestWhenAuditIdIsNegative(): void
    {
        $service = $this->newService();
        $result = $service->restoreFromLifecycleAudit(-1, 99);

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

    public function testRestoreReturnsInvalidRequestWhenActorUserIdIsZero(): void
    {
        $service = $this->newService();
        $result = $service->restoreFromLifecycleAudit(1, 0);

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

    public function testRestoreReturnsInvalidRequestWhenActorUserIdIsNegative(): void
    {
        $service = $this->newService();
        $result = $service->restoreFromLifecycleAudit(1, -5);

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

    // ── audit_event_not_found ───────────────────────────────────────────

    public function testRestoreReturnsAuditEventNotFoundWhenEventIsNull(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->expects($this->once())
            ->method('findDeleteEventForRestore')
            ->with(1, true)
            ->willReturn(null);

        $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(
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── audit_event_already_restored ────────────────────────────────────

    public function testRestoreReturnsAlreadyRestoredWhenEventHasRestoredAt(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn([
            'id' => 1,
            'restored_at' => '2025-01-01 00:00:00',
        ]);

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

        $service = $this->newService(
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── snapshot_unavailable ────────────────────────────────────────────

    public function testRestoreReturnsSnapshotUnavailableWhenDecryptReturnsNull(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn([
            'id' => 1,
            'restored_at' => null,
        ]);
        $auditService->method('decryptSnapshot')->willReturn(null);

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

        $service = $this->newService(
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── snapshot_invalid ────────────────────────────────────────────────

    public function testRestoreReturnsSnapshotInvalidWhenUuidIsMissing(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
        $auditService->method('decryptSnapshot')->willReturn(['uuid' => '', 'email' => 'a@b.com']);

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

        $service = $this->newService(
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    public function testRestoreReturnsSnapshotInvalidWhenEmailIsMissing(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
        $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => '']);

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

        $service = $this->newService(
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── restore_uuid_exists ─────────────────────────────────────────────

    public function testRestoreReturnsUuidExistsWhenUserWithUuidAlreadyExists(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
        $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'existing-uuid', 'email' => 'new@example.com']);

        $userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepo->expects($this->once())->method('findByUuid')->with('existing-uuid')
            ->willReturn(['id' => 50, 'uuid' => 'existing-uuid']);

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

        $service = $this->newService(
            userReadRepository: $userReadRepo,
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── restore_email_exists ────────────────────────────────────────────

    public function testRestoreReturnsEmailExistsWhenUserWithEmailAlreadyExists(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
        $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'new-uuid', 'email' => 'existing@example.com']);

        $userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepo->method('findByUuid')->willReturn(null);
        $userReadRepo->expects($this->once())->method('findByEmail')->with('existing@example.com')
            ->willReturn(['id' => 60, 'email' => 'existing@example.com']);

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

        $service = $this->newService(
            userReadRepository: $userReadRepo,
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── restore_create_failed ───────────────────────────────────────────

    public function testRestoreReturnsCreateFailedWhenWriteRepoReturnsZero(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
        $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']);

        $userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepo->method('findByUuid')->willReturn(null);
        $userReadRepo->method('findByEmail')->willReturn(null);

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

        $settingsGateway = $this->createMock(UserSettingsGateway::class);
        $settingsGateway->method('normalizeTheme')->willReturn('light');

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

        $service = $this->newService(
            userReadRepository: $userReadRepo,
            userWriteRepository: $userWriteRepo,
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession,
            userSettingsGateway: $settingsGateway
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    public function testRestoreReturnsCreateFailedWhenWriteRepoReturnsFalse(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
        $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']);

        $userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepo->method('findByUuid')->willReturn(null);
        $userReadRepo->method('findByEmail')->willReturn(null);

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

        $settingsGateway = $this->createMock(UserSettingsGateway::class);
        $settingsGateway->method('normalizeTheme')->willReturn('light');

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

        $service = $this->newService(
            userReadRepository: $userReadRepo,
            userWriteRepository: $userWriteRepo,
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession,
            userSettingsGateway: $settingsGateway
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── restore_mark_failed ─────────────────────────────────────────────

    public function testRestoreReturnsMarkFailedWhenMarkRestoredReturnsFalse(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
        $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']);
        $auditService->expects($this->once())
            ->method('markDeleteEventRestored')
            ->with(1, 99, 77)
            ->willReturn(false);

        $userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepo->method('findByUuid')->willReturn(null);
        $userReadRepo->method('findByEmail')->willReturn(null);

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

        $settingsGateway = $this->createMock(UserSettingsGateway::class);
        $settingsGateway->method('normalizeTheme')->willReturn('light');

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

        $service = $this->newService(
            userReadRepository: $userReadRepo,
            userWriteRepository: $userWriteRepo,
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession,
            userSettingsGateway: $settingsGateway
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    // ── Transaction rollback on exception ───────────────────────────────

    public function testRestoreRollsBackOnUnexpectedException(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')
            ->willThrowException(new \RuntimeException('unexpected'));

        $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(
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        $result = $service->restoreFromLifecycleAudit(1, 99);

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

    public function testRestoreRollbackQuietlySwallowsRollbackException(): void
    {
        $auditService = $this->createMock(UserLifecycleAuditInterface::class);
        $auditService->method('findDeleteEventForRestore')
            ->willThrowException(new \RuntimeException('db error'));

        $dbSession = $this->createMock(DatabaseSessionRepository::class);
        $dbSession->method('rollbackTransaction')
            ->willThrowException(new \RuntimeException('rollback also failed'));

        $service = $this->newService(
            userLifecycleAuditService: $auditService,
            databaseSessionRepository: $dbSession
        );

        // Should not throw, even though rollback itself throws
        $result = $service->restoreFromLifecycleAudit(1, 99);

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

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

    private function newService(
        ?UserReadRepositoryInterface $userReadRepository = null,
        ?UserWriteRepositoryInterface $userWriteRepository = null,
        ?UserLifecycleAuditInterface $userLifecycleAuditService = null,
        ?DatabaseSessionRepository $databaseSessionRepository = null,
        ?UserSettingsGateway $userSettingsGateway = null,
    ): UserLifecycleRestoreService {
        return new UserLifecycleRestoreService(
            $userReadRepository ?? $this->createMock(UserReadRepositoryInterface::class),
            $userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class),
            $userLifecycleAuditService ?? $this->createMock(UserLifecycleAuditInterface::class),
            $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class),
            $userSettingsGateway ?? $this->createMock(UserSettingsGateway::class),
        );
    }
}
