<?php

declare(strict_types=1);

namespace App\Tests\Unit\Auth;

use App\Domain\Auth\TokenIssuer;
use App\Domain\Auth\TokenKind;
use PHPUnit\Framework\TestCase;
use ReflectionClass;

/**
 * SPEC §M14.3: prove tokens carry the entropy the SPEC promises (160 bits
 * from a CSPRNG), and that the wire format is exactly
 * `irdb_<kind3>_<32 base32 chars>`.
 *
 * The "1000 distinct tokens" check is a probabilistic floor — at 160 bits,
 * the collision probability for 1000 samples is negligible (~1e-43), so
 * any failure here means something is very wrong upstream of `random_bytes`.
 */
final class TokenEntropyTest extends TestCase
{
    private const FORMAT = '/^irdb_(rep|con|adm|svc)_[A-Z2-7]{32}$/';

    public function testThousandTokensAllDistinct(): void
    {
        $issuer = new TokenIssuer();
        $set = [];
        for ($i = 0; $i < 1000; ++$i) {
            $set[$issuer->issue(TokenKind::Admin)] = true;
        }
        self::assertCount(1000, $set, 'expected 1000 distinct tokens; collision detected');
    }

    public function testEveryTokenMatchesPublishedFormat(): void
    {
        $issuer = new TokenIssuer();
        foreach (TokenKind::cases() as $kind) {
            for ($i = 0; $i < 50; ++$i) {
                $raw = $issuer->issue($kind);
                self::assertSame(
                    1,
                    preg_match(self::FORMAT, $raw),
                    "format mismatch for {$kind->value}: {$raw}"
                );
                self::assertStringStartsWith('irdb_' . $kind->code() . '_', $raw);
            }
        }
    }

    public function testIssuerSourcesEntropyFromRandomBytes(): void
    {
        // Static inspection: confirm `random_bytes` is the single source.
        // If a refactor swaps in `mt_rand`, `rand`, `microtime`, or anything
        // else not listed in PHP's CSPRNG API, this test fails loudly.
        $reflection = new ReflectionClass(TokenIssuer::class);
        $file = (string) $reflection->getFileName();
        self::assertNotEmpty($file);
        $source = (string) file_get_contents($file);

        self::assertStringContainsString('random_bytes(', $source, 'TokenIssuer must source entropy from random_bytes (CSPRNG)');
        $forbidden = ['mt_rand(', 'rand(', 'uniqid(', 'microtime(', 'srand('];
        foreach ($forbidden as $needle) {
            self::assertStringNotContainsString($needle, $source, "TokenIssuer must not use non-CSPRNG source: {$needle}");
        }
    }

    public function testRandomByteCountIsTwentyForOneSixtyBits(): void
    {
        // Re-derive 160 bits / 8 = 20 bytes and confirm the issuer asks
        // for that many. Catches future "let's use 16 bytes" regressions.
        $reflection = new ReflectionClass(TokenIssuer::class);
        $source = (string) file_get_contents((string) $reflection->getFileName());
        self::assertSame(
            1,
            preg_match('/random_bytes\(\s*20\s*\)/', $source),
            'TokenIssuer must request 20 bytes (160 bits) of entropy'
        );
    }
}
