<?php

declare(strict_types=1);

namespace App\Tests\Unit\Logging;

use App\Logging\SecretScrubbingProcessor;
use Monolog\Formatter\JsonFormatter;
use Monolog\Level;
use Monolog\LogRecord;
use PHPUnit\Framework\TestCase;

final class SecretScrubbingProcessorTest extends TestCase
{
    public function testBearerTokenInContextIsScrubbed(): void
    {
        $processor = new SecretScrubbingProcessor();
        $record = $this->record('outbound api call', [
            'authorization' => 'Bearer irdb_svc_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
        ]);

        $out = $processor($record);
        self::assertSame('***', $out->context['authorization']);
    }

    public function testFormattedOutputDoesNotLeakBearerToken(): void
    {
        $processor = new SecretScrubbingProcessor();
        $record = $this->record('outbound', [
            'headers' => ['Authorization' => 'Bearer irdb_svc_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
        ]);

        $out = $processor($record);
        $line = (new JsonFormatter())->format($out);

        self::assertStringNotContainsString('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', $line);
        self::assertStringContainsString('***', $line);
    }

    public function testLocalAdminPasswordHashKeyScrubbed(): void
    {
        $processor = new SecretScrubbingProcessor();
        $record = $this->record('config', [
            'LOCAL_ADMIN_PASSWORD_HASH' => '$argon2id$v=19$abc$def',
            'OIDC_CLIENT_SECRET' => 'oidc-secret',
        ]);

        $out = $processor($record);
        self::assertSame('***', $out->context['LOCAL_ADMIN_PASSWORD_HASH']);
        self::assertSame('***', $out->context['OIDC_CLIENT_SECRET']);
    }

    public function testNonSensitiveLeftAlone(): void
    {
        $processor = new SecretScrubbingProcessor();
        $record = $this->record('search ok', ['count' => 42, 'q' => '203.0.113.42']);

        $out = $processor($record);
        self::assertSame(42, $out->context['count']);
        self::assertSame('203.0.113.42', $out->context['q']);
    }

    public function testRawJwtInValueIsScrubbed(): void
    {
        // SEC_REVIEW F65: JWTs anchored on `eyJ` (base64url of `{"`)
        // are scrubbed regardless of the key they're logged under.
        $processor = new SecretScrubbingProcessor();
        $jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3In0.qj8u_Mt1ZyT5PfksI91X4Aaa';
        $record = $this->record('oidc id_token captured', ['jwt' => $jwt]);

        $out = $processor($record);
        self::assertSame('eyJ***', $out->context['jwt']);
    }

    public function testShortBearerIsScrubbed(): void
    {
        // SEC_REVIEW F65: Bearer floor lowered from {20,} to {8,}.
        $processor = new SecretScrubbingProcessor();
        $record = $this->record('outbound', ['auth' => 'Bearer abcd1234']);

        $out = $processor($record);
        self::assertSame('Bearer ***', $out->context['auth']);
    }

    /**
     * @param array<string, mixed> $context
     */
    private function record(string $message, array $context): LogRecord
    {
        return new LogRecord(
            datetime: new \DateTimeImmutable(),
            channel: 'test',
            level: Level::Info,
            message: $message,
            context: $context,
        );
    }
}
