<?php

namespace MintyPHP\Tests\Module\Audit\Service;

use MintyPHP\Module\Audit\Repository\UserLifecycleAuditRepository;
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
use MintyPHP\Support\Crypto;
use PHPUnit\Framework\TestCase;

class UserLifecycleAuditServiceTest extends TestCase
{
    private const SNAPSHOT_FIELDS = [
        'uuid',
        'first_name',
        'last_name',
        'display_name',
        'email',
        'profile_description',
        'job_title',
        'phone',
        'mobile',
        'short_dial',
        'address',
        'postal_code',
        'city',
        'region',
        'country',
        'hire_date',
        'locale',
        'theme',
        'email_verified_at',
        'last_login_at',
        'last_login_provider',
        'primary_tenant_id',
        'current_tenant_id',
        'created',
        'modified',
        'active_changed_at',
    ];

    private static bool $cryptoAvailable = false;

    public static function setUpBeforeClass(): void
    {
        if (!defined('APP_CRYPTO_KEY')) {
            define('APP_CRYPTO_KEY', str_repeat('ab', 32));
        }
        self::$cryptoAvailable = Crypto::isConfigured();
    }

    // ── logDeactivate ───────────────────────────────────────────────────

    public function testLogDeactivateCallsRepoCreateWithCorrectRow(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['action'] === 'deactivate'
                    && $row['trigger_type'] === 'manual'
                    && $row['status'] === 'success'
                    && $row['run_uuid'] === 'run-1'
                    && $row['target_user_id'] === 5
                    && $row['target_user_uuid'] === 'user-uuid-5'
                    && $row['target_user_email'] === 'test@example.com'
                    && $row['actor_user_id'] === 99
                    && $row['policy_deactivate_days'] === 30
                    && $row['policy_delete_days'] === 60
                    && $row['snapshot_enc'] === null
                    && $row['snapshot_version'] === 1;
            }))
            ->willReturn(1);

        $service = new UserLifecycleAuditService($repo);
        $result = $service->logDeactivate(
            'run-1',
            'manual',
            ['deactivate_days' => 30, 'delete_days' => 60],
            99,
            ['id' => 5, 'uuid' => 'user-uuid-5', 'email' => 'test@example.com'],
            'success',
            null
        );

        $this->assertTrue($result);
    }

    public function testLogDeactivateReturnsFalseOnRepoException(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->method('create')->willThrowException(new \RuntimeException('db error'));

        $service = new UserLifecycleAuditService($repo);
        $result = $service->logDeactivate('run-1', 'manual', [], null, []);

        $this->assertFalse($result);
    }

    public function testLogDeactivateReturnsFalseWhenRepoReturnsFalse(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->method('create')->willReturn(false);

        $service = new UserLifecycleAuditService($repo);
        $result = $service->logDeactivate('run-1', 'manual', [], null, []);

        $this->assertFalse($result);
    }

    // ── logDeleteWithSnapshot ───────────────────────────────────────────

    public function testLogDeleteWithSnapshotCreatesEncryptedSnapshot(): void
    {
        if (!self::$cryptoAvailable) {
            $this->markTestSkipped('Crypto not configured');
        }

        $targetUser = [];
        foreach (self::SNAPSHOT_FIELDS as $field) {
            $targetUser[$field] = 'val_' . $field;
        }
        $targetUser['id'] = 5;
        $targetUser['extra_field'] = 'should_be_excluded';

        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['action'] === 'delete'
                    && $row['status'] === 'success'
                    && $row['snapshot_version'] === 1
                    && is_string($row['snapshot_enc'])
                    && $row['snapshot_enc'] !== '';
            }))
            ->willReturn(7);

        $service = new UserLifecycleAuditService($repo);
        $result = $service->logDeleteWithSnapshot('run-2', 'cron', ['deactivate_days' => 30, 'delete_days' => 90], null, $targetUser);

        $this->assertSame(7, $result);
    }

    public function testLogDeleteWithSnapshotContainsOnlySnapshotFields(): void
    {
        if (!self::$cryptoAvailable) {
            $this->markTestSkipped('Crypto not configured');
        }

        $targetUser = ['id' => 5, 'uuid' => 'u5', 'email' => 'e@example.com', 'extra' => 'no'];

        $capturedRow = null;
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row) use (&$capturedRow): bool {
                $capturedRow = $row;
                return true;
            }))
            ->willReturn(1);

        $service = new UserLifecycleAuditService($repo);
        $service->logDeleteWithSnapshot('run-3', 'manual', [], null, $targetUser);

        $this->assertNotNull($capturedRow);
        $decryptedJson = Crypto::decryptString($capturedRow['snapshot_enc']);
        $snapshot = json_decode($decryptedJson, true);
        $this->assertIsArray($snapshot);
        $this->assertSame(self::SNAPSHOT_FIELDS, array_keys($snapshot));
        $this->assertArrayNotHasKey('extra', $snapshot);
        $this->assertArrayNotHasKey('id', $snapshot);
    }

    public function testLogDeleteWithSnapshotReturnsFalseOnCryptoFailure(): void
    {
        // Simulate a scenario where Crypto throws by passing a target user that will
        // cause the try/catch to fire. We cannot easily override static Crypto, but we
        // know that if APP_CRYPTO_KEY were missing, encryptString would throw.
        // Since we defined a valid key, let's test the repo exception path instead.
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->method('create')->willThrowException(new \RuntimeException('db error'));

        $service = new UserLifecycleAuditService($repo);
        $result = $service->logDeleteWithSnapshot('run-4', 'manual', [], null, ['id' => 1, 'uuid' => 'u']);

        $this->assertFalse($result);
    }

    // ── logRestore ──────────────────────────────────────────────────────

    public function testLogRestoreCallsRepoWithRestoreAction(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['action'] === 'restore'
                    && $row['trigger_type'] === 'manual'
                    && $row['status'] === 'success';
            }))
            ->willReturn(10);

        $service = new UserLifecycleAuditService($repo);
        $result = $service->logRestore('run-5', 'manual', [], 99, ['id' => 5, 'uuid' => 'u5', 'email' => 'e@x.com']);

        $this->assertTrue($result);
    }

    public function testLogRestoreReturnsFalseOnException(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->method('create')->willThrowException(new \RuntimeException('fail'));

        $service = new UserLifecycleAuditService($repo);
        $result = $service->logRestore('run-5', 'manual', [], null, []);

        $this->assertFalse($result);
    }

    // ── decryptSnapshot ─────────────────────────────────────────────────

    public function testDecryptSnapshotWithValidPayload(): void
    {
        if (!self::$cryptoAvailable) {
            $this->markTestSkipped('Crypto not configured');
        }

        $original = ['uuid' => 'test-uuid', 'email' => 'test@example.com'];
        $encrypted = Crypto::encryptString(json_encode($original));

        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $service = new UserLifecycleAuditService($repo);

        $result = $service->decryptSnapshot(['snapshot_enc' => $encrypted]);
        $this->assertSame($original, $result);
    }

    public function testDecryptSnapshotReturnsNullForEmptyPayload(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $service = new UserLifecycleAuditService($repo);

        $this->assertNull($service->decryptSnapshot([]));
        $this->assertNull($service->decryptSnapshot(['snapshot_enc' => '']));
        $this->assertNull($service->decryptSnapshot(['snapshot_enc' => '   ']));
    }

    public function testDecryptSnapshotReturnsNullForInvalidPayload(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $service = new UserLifecycleAuditService($repo);

        $result = $service->decryptSnapshot(['snapshot_enc' => 'not-valid-encrypted-data']);
        $this->assertNull($result);
    }

    // ── markDeleteEventRestored ─────────────────────────────────────────

    public function testMarkDeleteEventRestoredDelegatesToRepo(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('markRestored')
            ->with(1, 99, 200)
            ->willReturn(true);

        $service = new UserLifecycleAuditService($repo);
        $this->assertTrue($service->markDeleteEventRestored(1, 99, 200));
    }

    public function testMarkDeleteEventRestoredReturnsFalseOnException(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->method('markRestored')->willThrowException(new \RuntimeException('fail'));

        $service = new UserLifecycleAuditService($repo);
        $this->assertFalse($service->markDeleteEventRestored(1, 99, 200));
    }

    // ── purgeExpired ────────────────────────────────────────────────────

    public function testPurgeExpiredDelegatesToRepoWith365Days(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('purgeOlderThanDays')
            ->with(365)
            ->willReturn(5);

        $service = new UserLifecycleAuditService($repo);
        $this->assertSame(5, $service->purgeExpired());
    }

    // ── buildBaseRow enum normalization ──────────────────────────────────

    public function testBuildBaseRowNormalizesInvalidTriggerTypeToDefault(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['trigger_type'] === 'system'; // fallback for invalid trigger
            }))
            ->willReturn(1);

        $service = new UserLifecycleAuditService($repo);
        $service->logDeactivate('run-x', 'INVALID_TRIGGER', [], null, []);
    }

    public function testBuildBaseRowNormalizesInvalidStatusToFailed(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['status'] === 'failed'; // fallback for invalid status
            }))
            ->willReturn(1);

        $service = new UserLifecycleAuditService($repo);
        $service->logDeactivate('run-x', 'manual', [], null, [], 'INVALID_STATUS');
    }

    public function testBuildBaseRowNormalizesNullActorToNull(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['actor_user_id'] === null;
            }))
            ->willReturn(1);

        $service = new UserLifecycleAuditService($repo);
        $service->logDeactivate('run-x', 'manual', [], null, []);
    }

    public function testBuildBaseRowNormalizesZeroActorToNull(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['actor_user_id'] === null;
            }))
            ->willReturn(1);

        $service = new UserLifecycleAuditService($repo);
        $service->logDeactivate('run-x', 'manual', [], 0, []);
    }

    public function testBuildBaseRowSetsNegativePolicyDaysToZero(): void
    {
        $repo = $this->createMock(UserLifecycleAuditRepository::class);
        $repo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $row): bool {
                return $row['policy_deactivate_days'] === 0
                    && $row['policy_delete_days'] === 0;
            }))
            ->willReturn(1);

        $service = new UserLifecycleAuditService($repo);
        $service->logDeactivate('run-x', 'manual', ['deactivate_days' => -10, 'delete_days' => -5], null, []);
    }
}
