<?php

declare(strict_types=1);

namespace Apexl\Tests\User\UnitTests;

use apexl\Config\ConfigurationMock;
use apexl\Io\modules\user\services\twoFactorAuth\methods\TotpMethod;
use apexl\Io\TestInfrastructure\UnitTest;
use PHPUnit\Framework\Attributes\Test;
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
use RobThree\Auth\TwoFactorAuth;
use function apexl\Io\container;

final class TotpMethodTest extends UnitTest
{
    // RFC 6238 Appendix B test key: ASCII "12345678901234567890", base32-encoded.
    private const string RFC_SECRET = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ';

    private function method(): TotpMethod
    {
        return container()->make(TotpMethod::class, [
            'config' => new ConfigurationMock(['auth.two_factor.app_name' => 'CygnusTest']),
        ]);
    }

    /**
     * Frozen-time verification against a known RFC 6238 vector, proving the
     * library + our configuration agree on the algorithm and that the
     * discrepancy (drift) window behaves as passed.
     */
    #[Test]
    public function frozenTimeVectorAndWindow(): void
    {
        $tfa = new TwoFactorAuth(new BaconQrCodeProvider(4, '#ffffff', '#000000', 'svg'), 'CygnusTest');

        // T = 59s → time-step 1 → 6-digit TOTP code is 287082 (RFC 6238 SHA1).
        $this->assertSame('287082', $tfa->getCode(self::RFC_SECRET, 59));
        $this->assertTrue($tfa->verifyCode(self::RFC_SECRET, '287082', 0, 59));

        // An adjacent time-step's code is different, and only verifies against
        // T=59 once the drift window is widened by one period (discrepancy >= 1).
        $adjacent = $tfa->getCode(self::RFC_SECRET, 89);
        $this->assertNotSame('287082', $adjacent);
        $this->assertFalse($tfa->verifyCode(self::RFC_SECRET, $adjacent, 0, 59));
        $this->assertTrue($tfa->verifyCode(self::RFC_SECRET, $adjacent, 1, 59));

        // A code from far outside the window never verifies.
        $far = $tfa->getCode(self::RFC_SECRET, 59 + 3600);
        $this->assertFalse($tfa->verifyCode(self::RFC_SECRET, $far, 1, 59));
    }

    #[Test]
    public function verifiesTheCurrentCodeAndRejectsWrongOnes(): void
    {
        $method = $this->method();
        $secret = $method->generateSecret();

        // Derive the live code the same way the library does, then feed it back.
        $tfa = new TwoFactorAuth(new BaconQrCodeProvider(4, '#ffffff', '#000000', 'svg'), 'CygnusTest');
        $currentCode = $tfa->getCode($secret, time());

        $this->assertTrue($method->verify($secret, $currentCode, 1));
        $this->assertFalse($method->verify($secret, '000000', 0));
        $this->assertFalse($method->verify($secret, 'not-a-code'));
        $this->assertFalse($method->verify($secret, ''));
    }

    #[Test]
    public function generatesSecretsAndProvisioningArtifacts(): void
    {
        $method = $this->method();

        $this->assertSame('totp', $method->getMethodName());

        $secret = $method->generateSecret();
        $this->assertMatchesRegularExpression('/^[A-Z2-7]+$/', $secret); // base32
        $this->assertGreaterThanOrEqual(16, strlen($secret));

        $uri = $method->provisioningUri('user@example.com', $secret);
        $this->assertStringStartsWith('otpauth://totp/', $uri);
        $this->assertStringContainsString('CygnusTest', $uri);
        $this->assertStringContainsString(rawurlencode('user@example.com'), $uri);

        $qr = $method->qrCodeDataUri('user@example.com', $secret);
        $this->assertStringStartsWith('data:image/svg+xml', $qr);
    }
}
