<?php

namespace MintyPHP\Tests\Service\Auth;

use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Repository\Auth\ApiTokenRepositoryInterface;
use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Service\Auth\ApiTokenService;
use MintyPHP\Service\Auth\AuthSettingsGateway;
use MintyPHP\Service\Tenant\TenantScopeService;
use PHPUnit\Framework\TestCase;

class ApiTokenServiceTest extends TestCase
{
    // ---------------------------------------------------------------
    // create()
    // ---------------------------------------------------------------

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

        $result = $service->create(1, '  ');

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

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

        $result = $service->create(0, 'My Token');

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

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

        $result = $service->create(-5, 'My Token');

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

    public function testCreateReturnsUserInactiveWhenUserNotFound(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->expects($this->any())->method('find')->with(42)->willReturn(null);

        $service = $this->newService(userReadRepository: $userRepo);

        $result = $service->create(42, 'Token');

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

    public function testCreateReturnsUserInactiveWhenUserIsInactive(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->expects($this->any())->method('find')->with(42)->willReturn(['id' => 42, 'active' => 0]);

        $service = $this->newService(userReadRepository: $userRepo);

        $result = $service->create(42, 'Token');

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

    public function testCreateReturnsMaxTokensReachedWhenLimitHit(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->expects($this->any())->method('countActiveForUser')->with(1)->willReturn(10);

        $service = $this->newService(userReadRepository: $userRepo, apiTokenRepository: $tokenRepo);

        $result = $service->create(1, 'Token');

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

    public function testCreateReturnsTenantNotAssignedWhenUserLacksTenant(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('countActiveForUser')->willReturn(0);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->any())->method('getUserTenantIds')->with(1)->willReturn([2, 3]);

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            scopeGateway: $scopeGateway
        );

        $result = $service->create(1, 'Token', tenantId: 99);

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

    public function testCreateReturnsInvalidExpiresAtForPastDate(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('countActiveForUser')->willReturn(0);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getApiTokenMaxTtlDays')->willReturn(90);

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            settingsGateway: $settingsGateway
        );

        $result = $service->create(1, 'Token', expiresAt: '2020-01-01 00:00:00');

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

    public function testCreateReturnsCreateFailedWhenRepositoryReturnsNull(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('countActiveForUser')->willReturn(0);
        $tokenRepo->method('create')->willReturn(null);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getApiTokenMaxTtlDays')->willReturn(365);
        $settingsGateway->method('getApiTokenDefaultTtlDays')->willReturn(30);

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            settingsGateway: $settingsGateway
        );

        $result = $service->create(1, 'Token');

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

    public function testCreateReturnsCreateFailedWhenFindAfterInsertFails(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('countActiveForUser')->willReturn(0);
        $tokenRepo->method('create')->willReturn(55);
        $tokenRepo->expects($this->any())->method('find')->with(55)->willReturn(null);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getApiTokenMaxTtlDays')->willReturn(365);
        $settingsGateway->method('getApiTokenDefaultTtlDays')->willReturn(30);

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            settingsGateway: $settingsGateway
        );

        $result = $service->create(1, 'Token');

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

    public function testCreateSuccessReturnsTokenAndId(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('countActiveForUser')->willReturn(0);
        $tokenRepo->method('create')->willReturn(99);
        $tokenRepo->expects($this->any())->method('find')->with(99)->willReturn(['id' => 99, 'uuid' => 'abc-uuid-123']);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getApiTokenMaxTtlDays')->willReturn(365);
        $settingsGateway->method('getApiTokenDefaultTtlDays')->willReturn(30);

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            settingsGateway: $settingsGateway
        );

        $result = $service->create(1, 'My API Token');

        $this->assertTrue($result['ok']);
        $this->assertSame(99, $result['id']);
        $this->assertSame('abc-uuid-123', $result['uuid']);
        $this->assertStringContainsString(':', $result['token']);
        $this->assertNotEmpty($result['expires_at']);

        // Selector is 24-char hex, plain token is 64-char hex
        [$selector, $plain] = explode(':', $result['token'], 2);
        $this->assertSame(24, strlen($selector));
        $this->assertSame(64, strlen($plain));
    }

    public function testCreateWithTenantSucceedsWhenUserHasTenant(): void
    {
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('countActiveForUser')->willReturn(0);
        $tokenRepo->method('create')->willReturn(100);
        $tokenRepo->expects($this->any())->method('find')->with(100)->willReturn(['id' => 100, 'uuid' => 'uuid-t']);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->expects($this->any())->method('getUserTenantIds')->with(1)->willReturn([5, 10]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getApiTokenMaxTtlDays')->willReturn(365);
        $settingsGateway->method('getApiTokenDefaultTtlDays')->willReturn(30);

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            settingsGateway: $settingsGateway,
            scopeGateway: $scopeGateway
        );

        $result = $service->create(1, 'Tenant Token', tenantId: 5);

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

    // ---------------------------------------------------------------
    // validate()
    // ---------------------------------------------------------------

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

        $this->assertNull($service->validate(''));
    }

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

        $this->assertNull($service->validate('no-colon-here'));
    }

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

        $this->assertNull($service->validate(':plaintoken'));
    }

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

        $this->assertNull($service->validate('selector:'));
    }

    public function testValidateReturnsNullWhenSelectorNotFoundPerformsDummyHash(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->expects($this->any())->method('findBySelector')->with('abc123')->willReturn(null);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $this->assertNull($service->validate('abc123:sometoken'));
    }

    public function testValidateReturnsNullWhenTokenIsRevoked(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('findBySelector')->willReturn([
            'id' => 1,
            'user_id' => 5,
            'revoked_at' => '2024-01-01 00:00:00',
            'expires_at' => null,
            'token_hash' => hash('sha256', 'secret'),
        ]);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $this->assertNull($service->validate('sel:secret'));
    }

    public function testValidateReturnsNullWhenTokenIsExpired(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('findBySelector')->willReturn([
            'id' => 1,
            'user_id' => 5,
            'revoked_at' => null,
            'expires_at' => '2020-01-01 00:00:00',
            'token_hash' => hash('sha256', 'secret'),
        ]);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $this->assertNull($service->validate('sel:secret'));
    }

    public function testValidateReturnsNullWhenHashDoesNotMatch(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('findBySelector')->willReturn([
            'id' => 1,
            'user_id' => 5,
            'revoked_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 3600),
            'token_hash' => hash('sha256', 'correct-secret'),
        ]);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $this->assertNull($service->validate('sel:wrong-secret'));
    }

    public function testValidateReturnsNullWhenUserIsInactive(): void
    {
        $plainToken = 'mytesttoken123';
        $tokenHash = hash('sha256', $plainToken);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('findBySelector')->willReturn([
            'id' => 1,
            'user_id' => 5,
            'revoked_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 3600),
            'token_hash' => $tokenHash,
        ]);

        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->expects($this->any())->method('find')->with(5)->willReturn(['id' => 5, 'active' => 0]);

        $service = $this->newService(userReadRepository: $userRepo, apiTokenRepository: $tokenRepo);

        $this->assertNull($service->validate('sel:' . $plainToken));
    }

    public function testValidateReturnsNullWhenUserNotFound(): void
    {
        $plainToken = 'mytesttoken456';
        $tokenHash = hash('sha256', $plainToken);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('findBySelector')->willReturn([
            'id' => 1,
            'user_id' => 5,
            'revoked_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 3600),
            'token_hash' => $tokenHash,
        ]);

        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->expects($this->any())->method('find')->with(5)->willReturn(null);

        $service = $this->newService(userReadRepository: $userRepo, apiTokenRepository: $tokenRepo);

        $this->assertNull($service->validate('sel:' . $plainToken));
    }

    public function testValidateSuccessReturnsUserAndTokenAndUpdatesLastUsed(): void
    {
        $plainToken = 'validplaintoken789';
        $tokenHash = hash('sha256', $plainToken);

        $tokenRecord = [
            'id' => 10,
            'user_id' => 7,
            'revoked_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 86400),
            'token_hash' => $tokenHash,
            'tenant_id' => 3,
        ];

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->expects($this->any())->method('findBySelector')->with('mysel')->willReturn($tokenRecord);
        $tokenRepo->expects($this->once())
            ->method('updateLastUsed')
            ->with(10, '192.168.1.1');

        $userRecord = ['id' => 7, 'active' => 1, 'email' => 'user@test.com'];
        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->expects($this->any())->method('find')->with(7)->willReturn($userRecord);

        $requestRuntime = $this->makeRequestRuntime('192.168.1.1');

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            requestRuntime: $requestRuntime
        );

        $result = $service->validate('mysel:' . $plainToken);

        $this->assertNotNull($result);
        $this->assertSame($userRecord, $result['user']);
        $this->assertSame($tokenRecord, $result['token_record']);
        $this->assertSame(3, $result['tenant_id']);
    }

    public function testValidateSuccessWithNullTenantId(): void
    {
        $plainToken = 'notenant123';
        $tokenHash = hash('sha256', $plainToken);

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('findBySelector')->willReturn([
            'id' => 11,
            'user_id' => 8,
            'revoked_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 86400),
            'token_hash' => $tokenHash,
            'tenant_id' => null,
        ]);
        $tokenRepo->expects($this->once())->method('updateLastUsed');

        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->expects($this->any())->method('find')->with(8)->willReturn(['id' => 8, 'active' => 1]);

        $service = $this->newService(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            requestRuntime: $this->makeRequestRuntime('10.0.0.1')
        );

        $result = $service->validate('sel:' . $plainToken);

        $this->assertNotNull($result);
        $this->assertNull($result['tenant_id']);
    }

    // ---------------------------------------------------------------
    // rotateCurrentToken()
    // ---------------------------------------------------------------

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

        $result = $service->rotateCurrentToken(0, 1);

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

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

        $result = $service->rotateCurrentToken(1, 0);

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

    public function testRotateReturnsNotFoundWhenTokenDoesNotExist(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturn(null);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

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

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

    public function testRotateReturnsNotFoundWhenTokenBelongsToDifferentUser(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturn(['id' => 5, 'user_id' => 99, 'revoked_at' => null, 'expires_at' => null]);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $result = $service->rotateCurrentToken(1, 5);

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

    public function testRotateReturnsInvalidWhenTokenIsRevoked(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturn([
            'id' => 5,
            'user_id' => 1,
            'revoked_at' => '2024-01-01 00:00:00',
            'expires_at' => null,
            'name' => 'Test',
        ]);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $result = $service->rotateCurrentToken(1, 5);

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

    public function testRotateReturnsInvalidWhenTokenIsExpired(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturn([
            'id' => 5,
            'user_id' => 1,
            'revoked_at' => null,
            'expires_at' => '2020-01-01 00:00:00',
            'name' => 'Test',
        ]);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $result = $service->rotateCurrentToken(1, 5);

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

    public function testRotateSuccessRevokesOldAndCreatesNew(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturnCallback(function (int $id) {
            if ($id === 5) {
                return [
                    'id' => 5,
                    'user_id' => 1,
                    'revoked_at' => null,
                    'expires_at' => gmdate('Y-m-d H:i:s', time() + 86400),
                    'name' => 'Rotate Me',
                    'tenant_id' => 2,
                ];
            }
            // The newly created token
            return ['id' => $id, 'uuid' => 'new-uuid'];
        });
        $tokenRepo->method('countActiveForUserExcludingId')->willReturn(0);
        $tokenRepo->method('countActiveForUser')->willReturn(0);
        $tokenRepo->expects($this->any())->method('revoke')->with(5)->willReturn(true);
        $tokenRepo->method('create')->willReturn(50);

        $userRepo = $this->createMock(UserReadRepositoryInterface::class);
        $userRepo->method('find')->willReturn(['id' => 1, 'active' => 1]);

        $scopeGateway = $this->createMock(TenantScopeService::class);
        $scopeGateway->method('getUserTenantIds')->willReturn([2]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getApiTokenMaxTtlDays')->willReturn(365);
        $settingsGateway->method('getApiTokenDefaultTtlDays')->willReturn(30);

        $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(
            userReadRepository: $userRepo,
            apiTokenRepository: $tokenRepo,
            settingsGateway: $settingsGateway,
            scopeGateway: $scopeGateway,
            databaseSessionRepository: $dbSession
        );

        $result = $service->rotateCurrentToken(1, 5);

        $this->assertTrue($result['ok']);
        $this->assertStringContainsString(':', $result['token']);
        $this->assertSame(2, $result['tenant_id']);
    }

    public function testRotateRollsBackWhenRevokeFails(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturn([
            'id' => 5,
            'user_id' => 1,
            'revoked_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 86400),
            'name' => 'Test',
            'tenant_id' => null,
        ]);
        $tokenRepo->method('countActiveForUserExcludingId')->willReturn(0);
        $tokenRepo->method('revoke')->willReturn(false);

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

        $service = $this->newService(
            apiTokenRepository: $tokenRepo,
            databaseSessionRepository: $dbSession
        );

        $result = $service->rotateCurrentToken(1, 5);

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

    public function testRotateRollsBackOnException(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturn([
            'id' => 5,
            'user_id' => 1,
            'revoked_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 86400),
            'name' => 'Test',
            'tenant_id' => null,
        ]);
        $tokenRepo->method('countActiveForUserExcludingId')->willReturn(0);
        $tokenRepo->method('revoke')->willThrowException(new \RuntimeException('DB down'));

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

        $service = $this->newService(
            apiTokenRepository: $tokenRepo,
            databaseSessionRepository: $dbSession
        );

        $result = $service->rotateCurrentToken(1, 5);

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

    // ---------------------------------------------------------------
    // revoke()
    // ---------------------------------------------------------------

    public function testRevokeReturnsNotFoundWhenTokenDoesNotExist(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('find')->willReturn(null);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $result = $service->revoke(999);

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

    public function testRevokeSuccessReturnsOk(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->expects($this->any())->method('find')->with(10)->willReturn(['id' => 10]);
        $tokenRepo->expects($this->once())->method('revoke')->with(10);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

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

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

    // ---------------------------------------------------------------
    // revokeAllForUser()
    // ---------------------------------------------------------------

    public function testRevokeAllForUserReturnsCount(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->expects($this->once())
            ->method('revokeAllForUser')
            ->with(5, null)
            ->willReturn(3);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $result = $service->revokeAllForUser(5);

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

    public function testRevokeAllForUserWithTenantReturnsCount(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->expects($this->once())
            ->method('revokeAllForUser')
            ->with(5, 2)
            ->willReturn(1);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $result = $service->revokeAllForUser(5, 2);

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

    // ---------------------------------------------------------------
    // listForUser()
    // ---------------------------------------------------------------

    public function testListForUserReturnsArray(): void
    {
        $expected = [
            ['id' => 1, 'name' => 'Token A'],
            ['id' => 2, 'name' => 'Token B'],
        ];

        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->expects($this->once())
            ->method('listByUserId')
            ->with(7)
            ->willReturn($expected);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

        $this->assertSame($expected, $service->listForUser(7));
    }

    public function testListForUserReturnsEmptyArray(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepositoryInterface::class);
        $tokenRepo->method('listByUserId')->willReturn([]);

        $service = $this->newService(apiTokenRepository: $tokenRepo);

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

    // ---------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------

    /**
     * RequestRuntimeInterface has a method named "method()" which PHPUnit 12
     * cannot mock (reserved name). Use a simple anonymous-class stub instead.
     */
    private function makeRequestRuntime(string $ip = '127.0.0.1'): RequestRuntimeInterface
    {
        return new class ($ip) implements RequestRuntimeInterface {
            public function __construct(private readonly string $stubIp)
            {
            }

            public function method(): string
            {
                return 'GET';
            }

            public function path(): string
            {
                return '/';
            }

            public function ip(): string
            {
                return $this->stubIp;
            }

            public function userAgent(): string
            {
                return 'test';
            }

            public function queryParams(): array
            {
                return [];
            }

            public function isSecure(): bool
            {
                return false;
            }
        };
    }

    private function newService(
        ?UserReadRepositoryInterface $userReadRepository = null,
        ?ApiTokenRepositoryInterface $apiTokenRepository = null,
        ?AuthSettingsGateway $settingsGateway = null,
        ?TenantScopeService $scopeGateway = null,
        ?DatabaseSessionRepository $databaseSessionRepository = null,
        ?RequestRuntimeInterface $requestRuntime = null
    ): ApiTokenService {
        return new ApiTokenService(
            $userReadRepository ?? $this->createMock(UserReadRepositoryInterface::class),
            $apiTokenRepository ?? $this->createMock(ApiTokenRepositoryInterface::class),
            $settingsGateway ?? $this->createMock(AuthSettingsGateway::class),
            $scopeGateway ?? $this->createMock(TenantScopeService::class),
            $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class),
            $requestRuntime ?? $this->makeRequestRuntime()
        );
    }
}
