<?php

namespace MintyPHP\Tests\Service\Auth;

use MintyPHP\Repository\Auth\ApiTokenRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Auth\ApiTokenEndpointService;
use MintyPHP\Service\Tenant\TenantScopeService;
use PHPUnit\Framework\TestCase;

class ApiTokenEndpointServiceTest extends TestCase
{
    // ---------------------------------------------------------------
    // normalizeLimit()
    // ---------------------------------------------------------------

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

        $this->assertSame(25, $service->normalizeLimit(25));
    }

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

        $this->assertSame(50, $service->normalizeLimit(0));
    }

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

        $this->assertSame(100, $service->normalizeLimit(999));
    }

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

        $this->assertSame(50, $service->normalizeLimit('abc'));
    }

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

        $this->assertSame(1, $service->normalizeLimit(1));
    }

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

        $this->assertSame(100, $service->normalizeLimit(100));
    }

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

        $this->assertSame(50, $service->normalizeLimit(-5));
    }

    // ---------------------------------------------------------------
    // listSelfTokens()
    // ---------------------------------------------------------------

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

        $this->assertSame([], $service->listSelfTokens(0, 50, null, null));
    }

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

        $this->assertSame([], $service->listSelfTokens(-1, 50, null, null));
    }

    public function testListSelfTokensReturnsFormattedTokensWithoutHash(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->expects($this->any())->method('listByUserId')->with(5, 50)->willReturn([
            [
                'id' => 10,
                'uuid' => 'uuid-a',
                'name' => 'Token A',
                'selector' => 'abcdef1234567890abcdefgh',
                'token_hash' => 'should-not-appear',
                'tenant_id' => null,
                'last_used_at' => '2025-01-01 10:00:00',
                'last_ip' => '10.0.0.1',
                'expires_at' => '2025-12-31 23:59:59',
                'revoked_at' => null,
                'created' => '2025-01-01 00:00:00',
            ],
        ]);

        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('listByIds')->willReturn([]);

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

        $result = $service->listSelfTokens(5, 50, null, null);

        $this->assertCount(1, $result);
        $this->assertSame('uuid-a', $result[0]['uuid']);
        $this->assertSame('Token A', $result[0]['name']);
        $this->assertSame('abcdef12', $result[0]['selector_prefix']);
        $this->assertArrayNotHasKey('token_hash', $result[0]);
        $this->assertFalse($result[0]['is_current_token']);
    }

    public function testListSelfTokensMarksCurrentToken(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->method('listByUserId')->willReturn([
            [
                'id' => 10,
                'uuid' => 'uuid-a',
                'name' => 'Token A',
                'selector' => 'abcdef1234567890',
                'tenant_id' => null,
                'last_used_at' => null,
                'last_ip' => '',
                'expires_at' => null,
                'revoked_at' => null,
                'created' => null,
            ],
            [
                'id' => 20,
                'uuid' => 'uuid-b',
                'name' => 'Token B',
                'selector' => 'zyxwvuts87654321',
                'tenant_id' => null,
                'last_used_at' => null,
                'last_ip' => '',
                'expires_at' => null,
                'revoked_at' => null,
                'created' => null,
            ],
        ]);

        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('listByIds')->willReturn([]);

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

        $result = $service->listSelfTokens(5, 50, null, 20);

        $this->assertCount(2, $result);
        $this->assertFalse($result[0]['is_current_token']);
        $this->assertTrue($result[1]['is_current_token']);
    }

    public function testListSelfTokensFiltersByScopedTenant(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->method('listByUserId')->willReturn([
            [
                'id' => 10,
                'uuid' => 'uuid-a',
                'name' => 'Token A',
                'selector' => 'abcdef1234567890',
                'tenant_id' => 1,
                'last_used_at' => null,
                'last_ip' => '',
                'expires_at' => null,
                'revoked_at' => null,
                'created' => null,
            ],
            [
                'id' => 20,
                'uuid' => 'uuid-b',
                'name' => 'Token B',
                'selector' => 'zyxwvuts87654321',
                'tenant_id' => 2,
                'last_used_at' => null,
                'last_ip' => '',
                'expires_at' => null,
                'revoked_at' => null,
                'created' => null,
            ],
        ]);

        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('listByIds')->willReturn([
            ['id' => 1, 'uuid' => 'tenant-uuid-1'],
        ]);

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

        // Only tokens with tenant_id=1 should be returned
        $result = $service->listSelfTokens(5, 50, 1, null);

        $this->assertCount(1, $result);
        $this->assertSame('uuid-a', $result[0]['uuid']);
    }

    // ---------------------------------------------------------------
    // findSelfToken()
    // ---------------------------------------------------------------

    public function testFindSelfTokenReturnsNullForInvalidUuid(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->expects($this->any())->method('isUuid')->with('not-valid')->willReturn(false);

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

        $this->assertNull($service->findSelfToken('not-valid', 5, null, null));
    }

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

        $this->assertNull($service->findSelfToken('some-uuid', 0, null, null));
    }

    public function testFindSelfTokenReturnsNullWhenTokenNotFound(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->method('isUuid')->willReturn(true);
        $tokenRepo->method('findByUuidForUser')->willReturn(null);

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

        $this->assertNull($service->findSelfToken('valid-uuid', 5, null, null));
    }

    public function testFindSelfTokenReturnsNullWhenScopedTenantDoesNotMatch(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->method('isUuid')->willReturn(true);
        $tokenRepo->method('findByUuidForUser')->willReturn([
            'id' => 10,
            'uuid' => 'valid-uuid',
            'name' => 'Token',
            'selector' => 'abcdef12',
            'tenant_id' => 2,
            'last_used_at' => null,
            'last_ip' => '',
            'expires_at' => null,
            'revoked_at' => null,
            'created' => null,
        ]);

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

        // Scoped to tenant 1 but token is for tenant 2
        $this->assertNull($service->findSelfToken('valid-uuid', 5, 1, null));
    }

    public function testFindSelfTokenSuccess(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->method('isUuid')->willReturn(true);
        $tokenRepo->method('findByUuidForUser')->willReturn([
            'id' => 10,
            'uuid' => 'valid-uuid',
            'name' => 'My Token',
            'selector' => 'abcdef1234567890',
            'tenant_id' => null,
            'last_used_at' => '2025-01-01 00:00:00',
            'last_ip' => '10.0.0.1',
            'expires_at' => '2026-01-01 00:00:00',
            'revoked_at' => null,
            'created' => '2025-01-01 00:00:00',
        ]);

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

        $result = $service->findSelfToken('valid-uuid', 5, null, 10);

        $this->assertNotNull($result);
        $this->assertSame('valid-uuid', $result['uuid']);
        $this->assertSame('My Token', $result['name']);
        $this->assertSame('abcdef12', $result['selector_prefix']);
        $this->assertTrue($result['is_current_token']);
        $this->assertArrayNotHasKey('token_hash', $result);
    }

    // ---------------------------------------------------------------
    // revokeSelfToken()
    // ---------------------------------------------------------------

    public function testRevokeSelfTokenReturnsFalseForInvalidUuid(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->method('isUuid')->willReturn(false);

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

        $this->assertFalse($service->revokeSelfToken('bad-uuid', 5));
    }

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

        $this->assertFalse($service->revokeSelfToken('some-uuid', 0));
    }

    public function testRevokeSelfTokenSuccessReturnsTrue(): void
    {
        $tokenRepo = $this->createMock(ApiTokenRepository::class);
        $tokenRepo->method('isUuid')->willReturn(true);
        $tokenRepo->expects($this->once())
            ->method('revokeByUuidForUser')
            ->with('valid-uuid', 5)
            ->willReturn(true);

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

        $this->assertTrue($service->revokeSelfToken('valid-uuid', 5));
    }

    // ---------------------------------------------------------------
    // resolveSelfCreateTenant()
    // ---------------------------------------------------------------

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

        $result = $service->resolveSelfCreateTenant(5, null, '');

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

    public function testResolveSelfCreateTenantReturnsInvalidTenantUuid(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->expects($this->any())->method('findByUuid')->with('bad-uuid')->willReturn(null);

        $service = $this->newService(tenantRepository: $tenantRepo);

        $result = $service->resolveSelfCreateTenant(5, null, 'bad-uuid');

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

    public function testResolveSelfCreateTenantReturnsTenantScopedForbiddenWhenScopeResolveFails(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        // resolveTenantUuidById calls find() to resolve the scoped tenant
        $tenantRepo->method('find')->willReturn(null);

        $service = $this->newService(tenantRepository: $tenantRepo);

        // scopedTenantId is set but its UUID cannot be resolved
        $result = $service->resolveSelfCreateTenant(5, 99, '');

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

    public function testResolveSelfCreateTenantReturnsTenantScopedForbiddenWhenTenantMismatch(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->expects($this->any())->method('findByUuid')->with('tenant-uuid-2')->willReturn(['id' => 2, 'uuid' => 'tenant-uuid-2']);
        $tenantRepo->expects($this->any())->method('find')->with(1)->willReturn(['id' => 1, 'uuid' => 'tenant-uuid-1']);

        $service = $this->newService(tenantRepository: $tenantRepo);

        // Scoped to tenant 1, but requesting tenant 2
        $result = $service->resolveSelfCreateTenant(5, 1, 'tenant-uuid-2');

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

    public function testResolveSelfCreateTenantReturnsTenantNotAssigned(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->expects($this->any())->method('findByUuid')->with('tenant-uuid-3')->willReturn(['id' => 3, 'uuid' => 'tenant-uuid-3']);

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

        $service = $this->newService(
            tenantRepository: $tenantRepo,
            authScopeGateway: $scopeService
        );

        $result = $service->resolveSelfCreateTenant(5, null, 'tenant-uuid-3');

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

    public function testResolveSelfCreateTenantSuccessWithValidTenant(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->expects($this->any())->method('findByUuid')->with('tenant-uuid-1')->willReturn(['id' => 1, 'uuid' => 'tenant-uuid-1']);

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

        $service = $this->newService(
            tenantRepository: $tenantRepo,
            authScopeGateway: $scopeService
        );

        $result = $service->resolveSelfCreateTenant(5, null, 'tenant-uuid-1');

        $this->assertTrue($result['ok']);
        $this->assertSame(1, $result['tenant_id']);
        $this->assertSame('tenant-uuid-1', $result['tenant_uuid']);
    }

    // ---------------------------------------------------------------
    // parseExpiresAt()
    // ---------------------------------------------------------------

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

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

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

    public function testParseExpiresAtValidFutureDate(): void
    {
        $service = $this->newService();
        $futureDate = gmdate('Y-m-d H:i:s', time() + 86400 * 30);

        $result = $service->parseExpiresAt($futureDate);

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

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

        $result = $service->parseExpiresAt('2020-01-01 00:00:00');

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

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

        $result = $service->parseExpiresAt('not-a-date-at-all!!!');

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

    // ---------------------------------------------------------------
    // capExpiryToParent()
    // ---------------------------------------------------------------

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

        $result = $service->capExpiryToParent('2025-06-01 00:00:00', '2025-12-31 00:00:00');

        $this->assertSame('2025-06-01 00:00:00', $result);
    }

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

        $result = $service->capExpiryToParent('2026-06-01 00:00:00', '2025-12-31 00:00:00');

        $this->assertSame('2025-12-31 00:00:00', $result);
    }

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

        $result = $service->capExpiryToParent('2026-06-01 00:00:00', null);

        $this->assertSame('2026-06-01 00:00:00', $result);
    }

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

        $result = $service->capExpiryToParent('2026-06-01 00:00:00', '');

        $this->assertSame('2026-06-01 00:00:00', $result);
    }

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

        $result = $service->capExpiryToParent(null, '2025-12-31 00:00:00');

        $this->assertSame('2025-12-31 00:00:00', $result);
    }

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

        $result = $service->capExpiryToParent(null, null);

        $this->assertNull($result);
    }

    // ---------------------------------------------------------------
    // Factory helper
    // ---------------------------------------------------------------

    private function newService(
        ?ApiTokenRepository $apiTokenRepository = null,
        ?TenantRepository $tenantRepository = null,
        ?TenantScopeService $authScopeGateway = null
    ): ApiTokenEndpointService {
        return new ApiTokenEndpointService(
            $apiTokenRepository ?? $this->createMock(ApiTokenRepository::class),
            $tenantRepository ?? $this->createMock(TenantRepository::class),
            $authScopeGateway ?? $this->createMock(TenantScopeService::class)
        );
    }
}
